Ao importar fatura de cartão com PaymentDate configurado, a data efetiva da transação passa a ser o vencimento e a data de compra é preservada em original_date. Migration 013 adiciona a coluna original_date em transactions. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
127 lines
2.6 KiB
Go
127 lines
2.6 KiB
Go
package parser
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"financeiro-carvalho/internal/model"
|
|
)
|
|
|
|
func ParseCSV(r io.Reader, m model.CSVMapping) ([]model.ImportRow, []string) {
|
|
if m.DateFormat == "" {
|
|
m.DateFormat = "02/01/2006"
|
|
}
|
|
if m.DecimalSeparator == "" {
|
|
m.DecimalSeparator = ","
|
|
}
|
|
fieldSep := ','
|
|
if m.FieldSeparator == ";" {
|
|
fieldSep = ';'
|
|
}
|
|
|
|
reader := csv.NewReader(r)
|
|
reader.Comma = fieldSep
|
|
reader.LazyQuotes = true
|
|
reader.TrimLeadingSpace = true
|
|
|
|
var rows []model.ImportRow
|
|
var errs []string
|
|
lineNum := 0
|
|
|
|
for {
|
|
record, err := reader.Read()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
lineNum++
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("linha %d: %v", lineNum, err))
|
|
continue
|
|
}
|
|
if lineNum == 1 && m.HasHeader {
|
|
continue
|
|
}
|
|
|
|
maxCol := max3(m.DateColumn, m.AmountColumn, m.DescriptionColumn)
|
|
if len(record) <= maxCol {
|
|
errs = append(errs, fmt.Sprintf("linha %d: colunas insuficientes (%d)", lineNum, len(record)))
|
|
continue
|
|
}
|
|
|
|
dateStr := strings.TrimSpace(record[m.DateColumn])
|
|
amtStr := strings.TrimSpace(record[m.AmountColumn])
|
|
desc := cleanDescription(record[m.DescriptionColumn])
|
|
|
|
// Skip blank rows (e.g. empty line after header in some bank exports)
|
|
if dateStr == "" && amtStr == "" && desc == "" {
|
|
continue
|
|
}
|
|
|
|
t, err := time.Parse(m.DateFormat, dateStr)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("linha %d: data inválida %q", lineNum, dateStr))
|
|
continue
|
|
}
|
|
|
|
// Strip currency symbols (R$, $, €, etc.) keeping digits, comma, dot, minus
|
|
cleaned := strings.Map(func(r rune) rune {
|
|
if r >= '0' && r <= '9' || r == ',' || r == '.' || r == '-' {
|
|
return r
|
|
}
|
|
return -1
|
|
}, amtStr)
|
|
cleaned = strings.TrimSpace(cleaned)
|
|
|
|
if m.DecimalSeparator == "," {
|
|
cleaned = strings.ReplaceAll(cleaned, ".", "")
|
|
cleaned = strings.ReplaceAll(cleaned, ",", ".")
|
|
}
|
|
amt, err := strconv.ParseFloat(cleaned, 64)
|
|
if err != nil || cleaned == "" {
|
|
errs = append(errs, fmt.Sprintf("linha %d: valor inválido %q", lineNum, amtStr))
|
|
continue
|
|
}
|
|
|
|
if m.SkipNegative && amt < 0 {
|
|
continue
|
|
}
|
|
|
|
txType := "expense"
|
|
if !m.AllExpenses && amt > 0 {
|
|
txType = "income"
|
|
}
|
|
|
|
purchaseDate := t.Format("2006-01-02")
|
|
effectiveDate := purchaseDate
|
|
originalDate := ""
|
|
if m.PaymentDate != "" {
|
|
effectiveDate = m.PaymentDate
|
|
originalDate = purchaseDate
|
|
}
|
|
|
|
rows = append(rows, model.ImportRow{
|
|
Date: effectiveDate,
|
|
OriginalDate: originalDate,
|
|
Amount: math.Abs(amt),
|
|
Description: desc,
|
|
Type: txType,
|
|
})
|
|
}
|
|
return rows, errs
|
|
}
|
|
|
|
func max3(a, b, c int) int {
|
|
if b > a {
|
|
a = b
|
|
}
|
|
if c > a {
|
|
a = c
|
|
}
|
|
return a
|
|
}
|