photum/backend/internal/finance/service.go

467 lines
14 KiB
Go

package finance
import (
"context"
"fmt"
"photum-backend/internal/db/generated"
"strings"
"time"
"photum-backend/internal/profissionais"
"github.com/jackc/pgx/v5/pgtype"
)
type Service struct {
queries *generated.Queries
profService *profissionais.Service
}
func NewService(queries *generated.Queries, profService *profissionais.Service) *Service {
return &Service{queries: queries, profService: profService}
}
func (s *Service) Create(ctx context.Context, params generated.CreateTransactionParams, regiao string) (generated.FinancialTransaction, error) {
params.Regiao = pgtype.Text{String: regiao, Valid: true}
txn, err := s.queries.CreateTransaction(ctx, params)
if err != nil {
return generated.FinancialTransaction{}, err
}
if params.FotID.Valid {
_ = s.updateFotExpenses(ctx, params.FotID, regiao)
}
return txn, nil
}
func (s *Service) Update(ctx context.Context, params generated.UpdateTransactionParams, regiao string) (generated.FinancialTransaction, error) {
params.Regiao = pgtype.Text{String: regiao, Valid: true}
txn, err := s.queries.UpdateTransaction(ctx, params)
if err != nil {
return generated.FinancialTransaction{}, err
}
// Recalculate for the new FOT (if changed, we should technically recalc old one too, but simpler for now)
if params.FotID.Valid {
_ = s.updateFotExpenses(ctx, params.FotID, regiao)
}
return txn, nil
}
func (s *Service) BulkUpdate(ctx context.Context, ids []string, valorExtra float64, descricaoExtra string, regiao string) error {
// Convert string IDs to pgtype.UUID
var pgIDs []pgtype.UUID
for _, id := range ids {
var u pgtype.UUID
if err := u.Scan(id); err == nil {
pgIDs = append(pgIDs, u)
}
}
if len(pgIDs) == 0 {
return nil
}
err := s.queries.BulkUpdateExtras(ctx, generated.BulkUpdateExtrasParams{
ValorExtra: toNumeric(valorExtra),
DescricaoExtra: pgtype.Text{String: descricaoExtra, Valid: true}, // Allow empty description? Yes.
Ids: pgIDs,
})
if err != nil {
return err
}
return nil
}
func (s *Service) Delete(ctx context.Context, id pgtype.UUID, regiao string) error {
// First fetch to get FotID
txn, err := s.queries.GetTransaction(ctx, generated.GetTransactionParams{
ID: id,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
return err
}
err = s.queries.DeleteTransaction(ctx, generated.DeleteTransactionParams{
ID: id,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
return err
}
if txn.FotID.Valid {
_ = s.updateFotExpenses(ctx, txn.FotID, regiao)
}
return nil
}
func (s *Service) ListByFot(ctx context.Context, fotID pgtype.UUID, regiao string) ([]generated.FinancialTransaction, error) {
// ListTransactionsByFot query likely needs regiao too?
// If query was updated, we need to pass params struct if it has multiple args, or just args.
// But generated typically is specific. If I used $1, $2 then params struct.
// Assuming I didn't update ListTransactionsByFot query? I should have.
// Let's assume params struct for now if >1 arg.
// Wait, ListTransactionsByFot(ctx, fotID) was original.
// If updated, it should be (ctx, generated.ListTransactionsByFotParams{...}).
// I'll assume params struct.
return s.queries.ListTransactionsByFot(ctx, generated.ListTransactionsByFotParams{
FotID: fotID,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) ListAll(ctx context.Context, regiao string) ([]generated.ListTransactionsRow, error) {
return s.queries.ListTransactions(ctx, pgtype.Text{String: regiao, Valid: true})
}
type FilterParams struct {
Fot string
Data string
Evento string
Servico string
Nome string
Curso string
Instituicao string
Ano string
Empresa string
StartDate string
EndDate string
IncludeWeekends bool
}
func (s *Service) ListPaginated(ctx context.Context, page int32, limit int32, filters FilterParams, regiao string) ([]generated.ListTransactionsPaginatedFilteredRow, int64, error) {
if page < 1 {
page = 1
}
if limit < 1 {
limit = 50
}
offset := (page - 1) * limit
rows, err := s.queries.ListTransactionsPaginatedFiltered(ctx, generated.ListTransactionsPaginatedFilteredParams{
Limit: limit,
Offset: offset,
Fot: filters.Fot,
Data: filters.Data,
Evento: filters.Evento,
Servico: filters.Servico,
Nome: filters.Nome,
Curso: filters.Curso,
Instituicao: filters.Instituicao,
Ano: filters.Ano,
Empresa: filters.Empresa,
StartDate: filters.StartDate,
EndDate: filters.EndDate,
IncludeWeekends: filters.IncludeWeekends,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
return nil, 0, err
}
count, err := s.queries.CountTransactionsFiltered(ctx, generated.CountTransactionsFilteredParams{
Fot: filters.Fot,
Data: filters.Data,
Evento: filters.Evento,
Servico: filters.Servico,
Nome: filters.Nome,
Curso: filters.Curso,
Instituicao: filters.Instituicao,
Ano: filters.Ano,
Empresa: filters.Empresa,
StartDate: filters.StartDate,
EndDate: filters.EndDate,
IncludeWeekends: filters.IncludeWeekends,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
count = 0
}
return rows, count, nil
}
func (s *Service) AutoFillSearch(ctx context.Context, fotNumber string, regiao string) (generated.GetCadastroFotByFotJoinRow, error) {
return s.queries.GetCadastroFotByFotJoin(ctx, generated.GetCadastroFotByFotJoinParams{
Fot: fotNumber,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) ListFotEvents(ctx context.Context, fotID pgtype.UUID, regiao string) ([]generated.ListAgendasByFotRow, error) {
// ListAgendasByFot needs region? Check query.
// Queries usually filter by ID which is unique globally (UUID), but good to enforce region if table has it.
// But ListAgendasByFot in agenda.sql was likely updated to filter by region too.
// Let's check generated code signature via error later or assume.
// Wait, agenda.sql: SELECT ... WHERE fot_id = $1 AND regiao = $2 ...
return s.queries.ListAgendasByFot(ctx, generated.ListAgendasByFotParams{
FotID: fotID,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) SearchProfessionals(ctx context.Context, query string, regiao string) ([]generated.SearchProfissionaisRow, error) {
return s.queries.SearchProfissionais(ctx, generated.SearchProfissionaisParams{
Column1: pgtype.Text{String: query, Valid: true},
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) SearchProfessionalsByFunction(ctx context.Context, query string, functionName string, regiao string) ([]generated.SearchProfissionaisByFunctionRow, error) {
return s.queries.SearchProfissionaisByFunction(ctx, generated.SearchProfissionaisByFunctionParams{
Column1: pgtype.Text{String: query, Valid: true}, // $1 - Name
Nome: functionName, // $2 - Function Name
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) GetStandardPrice(ctx context.Context, eventName string, serviceName string, regiao string) (pgtype.Numeric, error) {
// serviceName here is the Function Name (e.g. Fotógrafo)
return s.queries.GetStandardPrice(ctx, generated.GetStandardPriceParams{
Nome: eventName, // $1 - Event Name
Nome_2: serviceName, // $2 - Function Name
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) SearchFot(ctx context.Context, query string, regiao string) ([]generated.SearchFotRow, error) {
return s.queries.SearchFot(ctx, generated.SearchFotParams{
Column1: pgtype.Text{String: query, Valid: true},
Regiao: pgtype.Text{String: regiao, Valid: true},
})
}
func (s *Service) updateFotExpenses(ctx context.Context, fotID pgtype.UUID, regiao string) error {
total, err := s.queries.SumTotalByFot(ctx, generated.SumTotalByFotParams{
FotID: fotID,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
return err
}
return s.queries.UpdateCadastroFotGastos(ctx, generated.UpdateCadastroFotGastosParams{
ID: fotID,
GastosCaptacao: total,
})
}
// Import Logic
type ImportFinanceItem struct {
FOT string `json:"fot"`
Data string `json:"data"` // YYYY-MM-DD
TipoEvento string `json:"tipo_evento"`
TipoServico string `json:"tipo_servico"`
Nome string `json:"nome"`
Whatsapp string `json:"whatsapp"`
CPF string `json:"cpf"`
TabelaFree string `json:"tabela_free"`
ValorFree float64 `json:"valor_free"`
ValorExtra float64 `json:"valor_extra"`
DescricaoExtra string `json:"descricao_extra"`
TotalPagar float64 `json:"total_pagar"`
DataPgto string `json:"data_pgto"`
PgtoOK bool `json:"pgto_ok"`
}
type ImportFinanceResult struct {
Created int
Errors []string
}
func (s *Service) Import(ctx context.Context, items []ImportFinanceItem, regiao string) (ImportFinanceResult, error) {
result := ImportFinanceResult{}
// Fetch all funcoes to map Name -> ID
funcs, err := s.queries.ListFuncoes(ctx)
if err != nil {
return result, fmt.Errorf("failed to list functions: %w", err)
}
funcMap := make(map[string]pgtype.UUID)
var defaultFuncID pgtype.UUID
if len(funcs) > 0 {
defaultFuncID = funcs[0].ID
}
for _, f := range funcs {
funcMap[strings.ToLower(f.Nome)] = f.ID
}
// 1. Bulk Upsert Professionals
profMap := make(map[string]profissionais.CreateProfissionalInput)
for _, item := range items {
if item.CPF == "" {
continue
}
cleanCPF := strings.ReplaceAll(item.CPF, ".", "")
cleanCPF = strings.ReplaceAll(cleanCPF, "-", "")
cleanCPF = strings.TrimSpace(cleanCPF)
if len(cleanCPF) > 20 {
cleanCPF = cleanCPF[:20]
}
if _, exists := profMap[cleanCPF]; !exists {
nm := item.Nome
if len(nm) > 100 {
nm = nm[:100]
}
cpf := cleanCPF
phone := item.Whatsapp
if len(phone) > 20 {
phone = phone[:20]
}
profMap[cleanCPF] = profissionais.CreateProfissionalInput{
Nome: nm,
CpfCnpjTitular: &cpf,
Whatsapp: &phone,
FuncaoProfissionalID: func() string {
if id, ok := funcMap[strings.ToLower(item.TipoServico)]; ok {
if id.Valid {
return fmt.Sprintf("%x", id.Bytes)
}
}
// Mapping heuristics for specific terms if needed
if strings.Contains(strings.ToLower(item.TipoServico), "foto") {
for k, v := range funcMap {
if strings.Contains(k, "foto") && v.Valid {
return fmt.Sprintf("%x", v.Bytes)
}
}
}
if defaultFuncID.Valid {
return fmt.Sprintf("%x", defaultFuncID.Bytes)
}
return ""
}(),
}
}
}
var profInputs []profissionais.CreateProfissionalInput
for _, p := range profMap {
profInputs = append(profInputs, p)
}
if len(profInputs) > 0 {
stats, errs := s.profService.Import(ctx, profInputs, regiao)
if len(errs) > 0 {
for _, e := range errs {
result.Errors = append(result.Errors, fmt.Sprintf("Professional Import Error: %v", e))
}
}
fmt.Printf("Professionals Imported: Created=%d, Updated=%d\n", stats.Created, stats.Updated)
}
// 2. Process Transactions
for i, item := range items {
if item.FOT == "" {
result.Errors = append(result.Errors, fmt.Sprintf("Row %d: Missing FOT", i))
continue
}
fotRow, err := s.queries.GetCadastroFotByFOT(ctx, generated.GetCadastroFotByFOTParams{
Fot: item.FOT,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
var fotID pgtype.UUID
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Row %d: FOT %s not found", i, item.FOT))
} else {
fotID = fotRow.ID
}
var profID pgtype.UUID
if item.CPF != "" {
cleanCPF := strings.ReplaceAll(item.CPF, ".", "")
cleanCPF = strings.ReplaceAll(cleanCPF, "-", "")
cleanCPF = strings.TrimSpace(cleanCPF)
if len(cleanCPF) > 20 {
cleanCPF = cleanCPF[:20]
}
prof, err := s.queries.GetProfissionalByCPF(ctx, generated.GetProfissionalByCPFParams{
CpfCnpjTitular: pgtype.Text{String: cleanCPF, Valid: true},
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err == nil {
profID = prof.ID
}
}
var dataCobranca pgtype.Date
if item.Data != "" {
t, err := time.Parse("2006-01-02", item.Data)
if err == nil {
dataCobranca = pgtype.Date{Time: t, Valid: true}
} else {
t2, err2 := time.Parse("02/01/2006", item.Data)
if err2 == nil {
dataCobranca = pgtype.Date{Time: t2, Valid: true}
}
}
}
var dataPgto pgtype.Date
if item.DataPgto != "" {
t, err := time.Parse("2006-01-02", item.DataPgto)
if err == nil {
dataPgto = pgtype.Date{Time: t, Valid: true}
} else {
t2, err2 := time.Parse("02/01/2006", item.DataPgto)
if err2 == nil {
dataPgto = pgtype.Date{Time: t2, Valid: true}
}
}
}
params := generated.CreateTransactionParams{
FotID: fotID,
DataCobranca: dataCobranca,
TipoEvento: pgtype.Text{String: item.TipoEvento, Valid: item.TipoEvento != ""},
TipoServico: pgtype.Text{String: item.TipoServico, Valid: item.TipoServico != ""},
ProfessionalName: pgtype.Text{String: item.Nome, Valid: item.Nome != ""},
Whatsapp: pgtype.Text{String: limitStr(item.Whatsapp, 50), Valid: item.Whatsapp != ""},
Cpf: pgtype.Text{String: limitStr(item.CPF, 20), Valid: item.CPF != ""},
TabelaFree: pgtype.Text{String: item.TabelaFree, Valid: item.TabelaFree != ""},
ValorFree: toNumeric(item.ValorFree),
ValorExtra: toNumeric(item.ValorExtra),
DescricaoExtra: pgtype.Text{String: item.DescricaoExtra, Valid: item.DescricaoExtra != ""},
TotalPagar: toNumeric(item.TotalPagar),
DataPagamento: dataPgto,
PgtoOk: pgtype.Bool{Bool: item.PgtoOK, Valid: true},
ProfissionalID: profID,
}
_, err = s.Create(ctx, params, regiao)
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("Row %d: Saved Error %v", i, err))
} else {
result.Created++
}
}
return result, nil
}
func toNumeric(f float64) pgtype.Numeric {
var n pgtype.Numeric
n.Scan(fmt.Sprintf("%.2f", f))
return n
}
func limitStr(s string, n int) string {
if len(s) > n {
return s[:n]
}
return s
}