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) (generated.FinancialTransaction, error) { txn, err := s.queries.CreateTransaction(ctx, params) if err != nil { return generated.FinancialTransaction{}, err } if params.FotID.Valid { _ = s.updateFotExpenses(ctx, params.FotID) } return txn, nil } func (s *Service) Update(ctx context.Context, params generated.UpdateTransactionParams) (generated.FinancialTransaction, error) { 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) } return txn, nil } func (s *Service) Delete(ctx context.Context, id pgtype.UUID) error { // First fetch to get FotID txn, err := s.queries.GetTransaction(ctx, id) if err != nil { return err } err = s.queries.DeleteTransaction(ctx, id) if err != nil { return err } if txn.FotID.Valid { _ = s.updateFotExpenses(ctx, txn.FotID) } return nil } func (s *Service) ListByFot(ctx context.Context, fotID pgtype.UUID) ([]generated.FinancialTransaction, error) { return s.queries.ListTransactionsByFot(ctx, fotID) } func (s *Service) ListAll(ctx context.Context) ([]generated.ListTransactionsRow, error) { return s.queries.ListTransactions(ctx) } type FilterParams struct { Fot string Data string Evento string Servico string Nome string } func (s *Service) ListPaginated(ctx context.Context, page int32, limit int32, filters FilterParams) ([]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, }) 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, }) if err != nil { count = 0 } return rows, count, nil } func (s *Service) AutoFillSearch(ctx context.Context, fotNumber string) (generated.GetCadastroFotByFotJoinRow, error) { return s.queries.GetCadastroFotByFotJoin(ctx, fotNumber) } func (s *Service) ListFotEvents(ctx context.Context, fotID pgtype.UUID) ([]generated.ListAgendasByFotRow, error) { return s.queries.ListAgendasByFot(ctx, fotID) } func (s *Service) SearchProfessionals(ctx context.Context, query string) ([]generated.SearchProfissionaisRow, error) { return s.queries.SearchProfissionais(ctx, pgtype.Text{String: query, Valid: true}) } func (s *Service) SearchProfessionalsByFunction(ctx context.Context, query string, functionName 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 }) } func (s *Service) GetStandardPrice(ctx context.Context, eventName string, serviceName 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 }) } func (s *Service) SearchFot(ctx context.Context, query string) ([]generated.SearchFotRow, error) { return s.queries.SearchFot(ctx, pgtype.Text{String: query, Valid: true}) } func (s *Service) updateFotExpenses(ctx context.Context, fotID pgtype.UUID) error { total, err := s.queries.SumTotalByFot(ctx, fotID) 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) (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) 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, item.FOT) 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, pgtype.Text{String: cleanCPF, 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) 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 }