- TransactionRepository.CategoryByDescription: busca a categoria mais recente para cada descrição normalizada no histórico do perfil - ImportService.Preview: pré-preenche category_id em linhas não-duplicatas cuja descrição já foi categorizada antes (case-insensitive) - Duplicatas não recebem sugestão - 2 novos testes: auto-categorize + duplicate-not-categorized Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"financeiro-carvalho/internal/model"
|
|
"financeiro-carvalho/internal/service"
|
|
)
|
|
|
|
// mockImportRepo implements repository.TransactionRepository for import tests.
|
|
type mockImportRepo struct {
|
|
catByDesc map[string]int // normalized desc → category_id
|
|
}
|
|
|
|
func (m *mockImportRepo) IsDuplicate(_ context.Context, _, _ string, _ float64) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (m *mockImportRepo) IsExternalIDKnown(_ context.Context, _ string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (m *mockImportRepo) BulkInsert(_ context.Context, rows []model.ImportRow) (int, error) {
|
|
return len(rows), nil
|
|
}
|
|
func (m *mockImportRepo) SaveImportLog(_ context.Context, _, _ string, _, _, _ int) error {
|
|
return nil
|
|
}
|
|
func (m *mockImportRepo) CategoryByDescription(_ context.Context, descs []string) (map[string]int, error) {
|
|
out := map[string]int{}
|
|
for _, d := range descs {
|
|
if id, ok := m.catByDesc[d]; ok {
|
|
out[d] = id
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
var csvMapping = model.CSVMapping{
|
|
DateColumn: 0,
|
|
AmountColumn: 1,
|
|
DescriptionColumn: 2,
|
|
DateFormat: "02/01/2006",
|
|
HasHeader: false,
|
|
DecimalSeparator: ",",
|
|
FieldSeparator: ";",
|
|
}
|
|
|
|
func TestPreview_AutoCategorize(t *testing.T) {
|
|
repo := &mockImportRepo{
|
|
catByDesc: map[string]int{
|
|
"padaria do ze": 3,
|
|
"uber *trip": 7,
|
|
},
|
|
}
|
|
svc := service.NewImportService(repo)
|
|
|
|
csv := "01/03/2024;-45,00;PADARIA DO ZE\n02/03/2024;-22,50;UBER *TRIP\n03/03/2024;-99,00;LOJA NOVA XYZ\n"
|
|
|
|
rows, _, err := svc.Preview(context.Background(), "fatura.csv", strings.NewReader(csv), &csvMapping)
|
|
if err != nil {
|
|
t.Fatalf("preview error: %v", err)
|
|
}
|
|
if len(rows) != 3 {
|
|
t.Fatalf("expected 3 rows, got %d", len(rows))
|
|
}
|
|
|
|
// PADARIA DO ZE → category 3
|
|
if rows[0].CategoryID == nil || *rows[0].CategoryID != 3 {
|
|
t.Errorf("row[0] (PADARIA DO ZE) expected category 3, got %v", rows[0].CategoryID)
|
|
}
|
|
// UBER *TRIP → category 7
|
|
if rows[1].CategoryID == nil || *rows[1].CategoryID != 7 {
|
|
t.Errorf("row[1] (UBER *TRIP) expected category 7, got %v", rows[1].CategoryID)
|
|
}
|
|
// LOJA NOVA XYZ → no suggestion
|
|
if rows[2].CategoryID != nil {
|
|
t.Errorf("row[2] (LOJA NOVA XYZ) expected no category, got %v", rows[2].CategoryID)
|
|
}
|
|
}
|
|
|
|
func TestPreview_DuplicateNotAutoCategorizated(t *testing.T) {
|
|
// row that IS a duplicate should not get category pre-filled
|
|
repo := &mockImportRepoDup{
|
|
catByDesc: map[string]int{"padaria do ze": 3},
|
|
}
|
|
svc := service.NewImportService(repo)
|
|
|
|
csv := "01/03/2024;-45,00;PADARIA DO ZE\n"
|
|
|
|
rows, _, err := svc.Preview(context.Background(), "fatura.csv", strings.NewReader(csv), &csvMapping)
|
|
if err != nil {
|
|
t.Fatalf("preview error: %v", err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("expected 1 row, got %d", len(rows))
|
|
}
|
|
if !rows[0].IsDuplicate {
|
|
t.Error("expected row to be marked duplicate")
|
|
}
|
|
if rows[0].CategoryID != nil {
|
|
t.Errorf("duplicate should not get category, got %v", rows[0].CategoryID)
|
|
}
|
|
}
|
|
|
|
// mockImportRepoDup marks all rows as duplicate (by date+amount+desc).
|
|
type mockImportRepoDup struct {
|
|
catByDesc map[string]int
|
|
}
|
|
|
|
func (m *mockImportRepoDup) IsDuplicate(_ context.Context, _, _ string, _ float64) (bool, error) {
|
|
return true, nil // always duplicate
|
|
}
|
|
func (m *mockImportRepoDup) IsExternalIDKnown(_ context.Context, _ string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (m *mockImportRepoDup) BulkInsert(_ context.Context, rows []model.ImportRow) (int, error) {
|
|
return len(rows), nil
|
|
}
|
|
func (m *mockImportRepoDup) SaveImportLog(_ context.Context, _, _ string, _, _, _ int) error {
|
|
return nil
|
|
}
|
|
func (m *mockImportRepoDup) CategoryByDescription(_ context.Context, descs []string) (map[string]int, error) {
|
|
out := map[string]int{}
|
|
for _, d := range descs {
|
|
if id, ok := m.catByDesc[d]; ok {
|
|
out[d] = id
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|