467 lines
14 KiB
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
|
|
}
|