feat: #46 auto-categorização no import por histórico de descrições

- 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]>
This commit is contained in:
2026-05-28 22:49:44 -03:00
co-authored by Claude Sonnet 4.6
parent 26eabc460b
commit 00356971c7
4 changed files with 195 additions and 1 deletions
+23 -1
View File
@@ -12,6 +12,10 @@ import (
"financeiro-carvalho/internal/repository"
)
func normalizeDesc(s string) string {
return strings.ToLower(strings.Join(strings.Fields(s), " "))
}
type ImportService struct {
repo repository.TransactionRepository
}
@@ -20,19 +24,37 @@ func NewImportService(repo repository.TransactionRepository) *ImportService {
return &ImportService{repo: repo}
}
// Preview parses the file and marks duplicates without saving anything.
// Preview parses the file, marks duplicates, and pre-fills category_id from history.
func (s *ImportService) Preview(ctx context.Context, filename string, r io.Reader, csvMapping *model.CSVMapping) ([]model.ImportRow, []string, error) {
rows, parseErrs, err := s.parse(filename, r, csvMapping)
if err != nil {
return nil, nil, err
}
// Collect unique descriptions for history lookup
descs := make([]string, 0, len(rows))
seen := make(map[string]bool)
for _, row := range rows {
n := normalizeDesc(row.Description)
if !seen[n] {
descs = append(descs, n)
seen[n] = true
}
}
catByDesc, _ := s.repo.CategoryByDescription(ctx, descs)
for i := range rows {
dup, err := s.isDuplicate(ctx, &rows[i])
if err != nil {
return nil, nil, fmt.Errorf("dedup check: %w", err)
}
rows[i].IsDuplicate = dup
// Pre-fill category from history for non-duplicate rows that have no category yet
if !dup && rows[i].CategoryID == nil {
if catID, ok := catByDesc[normalizeDesc(rows[i].Description)]; ok {
rows[i].CategoryID = &catID
}
}
}
return rows, parseErrs, nil
}