feat: salvar data original da transação e data de vencimento no cartão
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]>
This commit is contained in:
@@ -44,6 +44,9 @@ var m011 string
|
|||||||
//go:embed sql/012_category_is_tax.sql
|
//go:embed sql/012_category_is_tax.sql
|
||||||
var m012 string
|
var m012 string
|
||||||
|
|
||||||
|
//go:embed sql/013_transaction_original_date.sql
|
||||||
|
var m013 string
|
||||||
|
|
||||||
func Run(ctx context.Context, pool *pgxpool.Pool) error {
|
func Run(ctx context.Context, pool *pgxpool.Pool) error {
|
||||||
// Bootstrap: ensure schema_migrations table exists before checking versions.
|
// Bootstrap: ensure schema_migrations table exists before checking versions.
|
||||||
if _, err := pool.Exec(ctx, `
|
if _, err := pool.Exec(ctx, `
|
||||||
@@ -55,7 +58,7 @@ func Run(ctx context.Context, pool *pgxpool.Pool) error {
|
|||||||
return fmt.Errorf("bootstrap schema_migrations: %w", err)
|
return fmt.Errorf("bootstrap schema_migrations: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012}
|
migrations := []string{m001, m002, m003, m004, m005, m006, m007, m008, m009, m010, m011, m012, m013}
|
||||||
for i, sql := range migrations {
|
for i, sql := range migrations {
|
||||||
version := i + 1
|
version := i + 1
|
||||||
var applied bool
|
var applied bool
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE transactions ADD COLUMN IF NOT EXISTS original_date DATE;
|
||||||
@@ -17,7 +17,8 @@ type Transaction struct {
|
|||||||
|
|
||||||
// ImportRow is a parsed-but-not-yet-saved transaction from a file.
|
// ImportRow is a parsed-but-not-yet-saved transaction from a file.
|
||||||
type ImportRow struct {
|
type ImportRow struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"` // effective date (payment date for credit cards)
|
||||||
|
OriginalDate string `json:"original_date"` // purchase date from the CSV (empty for non-credit imports)
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -44,4 +45,5 @@ type CSVMapping struct {
|
|||||||
FieldSeparator string `json:"field_separator"` // "," or ";"
|
FieldSeparator string `json:"field_separator"` // "," or ";"
|
||||||
SkipNegative bool `json:"skip_negative"` // skip rows with negative amount (e.g. credit card bill payments)
|
SkipNegative bool `json:"skip_negative"` // skip rows with negative amount (e.g. credit card bill payments)
|
||||||
AllExpenses bool `json:"all_expenses"` // treat all rows as expense (credit card statements)
|
AllExpenses bool `json:"all_expenses"` // treat all rows as expense (credit card statements)
|
||||||
|
PaymentDate string `json:"payment_date"` // YYYY-MM-DD; when set, overrides date for all rows (credit card due date)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,8 +96,17 @@ func ParseCSV(r io.Reader, m model.CSVMapping) ([]model.ImportRow, []string) {
|
|||||||
txType = "income"
|
txType = "income"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
purchaseDate := t.Format("2006-01-02")
|
||||||
|
effectiveDate := purchaseDate
|
||||||
|
originalDate := ""
|
||||||
|
if m.PaymentDate != "" {
|
||||||
|
effectiveDate = m.PaymentDate
|
||||||
|
originalDate = purchaseDate
|
||||||
|
}
|
||||||
|
|
||||||
rows = append(rows, model.ImportRow{
|
rows = append(rows, model.ImportRow{
|
||||||
Date: t.Format("2006-01-02"),
|
Date: effectiveDate,
|
||||||
|
OriginalDate: originalDate,
|
||||||
Amount: math.Abs(amt),
|
Amount: math.Abs(amt),
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Type: txType,
|
Type: txType,
|
||||||
|
|||||||
@@ -62,10 +62,14 @@ func (r *transactionRepo) BulkInsert(ctx context.Context, rows []model.ImportRow
|
|||||||
if row.ExternalID != "" {
|
if row.ExternalID != "" {
|
||||||
extID = &row.ExternalID
|
extID = &row.ExternalID
|
||||||
}
|
}
|
||||||
|
var origDate *string
|
||||||
|
if row.OriginalDate != "" {
|
||||||
|
origDate = &row.OriginalDate
|
||||||
|
}
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
INSERT INTO transactions (date, amount, description, type, source, external_id, category_id)
|
INSERT INTO transactions (date, original_date, amount, description, type, source, external_id, category_id)
|
||||||
VALUES ($1, $2, $3, $4, 'import', $5, $6)`,
|
VALUES ($1, $2, $3, $4, $5, 'import', $6, $7)`,
|
||||||
row.Date, row.Amount, row.Description, row.Type, extID, row.CategoryID)
|
row.Date, origDate, row.Amount, row.Description, row.Type, extID, row.CategoryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface CSVMapping {
|
|||||||
field_separator: string
|
field_separator: string
|
||||||
skip_negative: boolean
|
skip_negative: boolean
|
||||||
all_expenses: boolean
|
all_expenses: boolean
|
||||||
|
payment_date: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useImportStore = defineStore('import', () => {
|
export const useImportStore = defineStore('import', () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const csvMapping = ref<CSVMapping>({
|
|||||||
field_separator: ';',
|
field_separator: ';',
|
||||||
skip_negative: false,
|
skip_negative: false,
|
||||||
all_expenses: false,
|
all_expenses: false,
|
||||||
|
payment_date: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Quando "Fatura de cartão" é marcado, configura automaticamente as colunas corretas
|
// Quando "Fatura de cartão" é marcado, configura automaticamente as colunas corretas
|
||||||
@@ -160,6 +161,10 @@ function fmt(amount: number) {
|
|||||||
<input type="checkbox" v-model="csvMapping.skip_negative" />
|
<input type="checkbox" v-model="csvMapping.skip_negative" />
|
||||||
Ignorar valores negativos
|
Ignorar valores negativos
|
||||||
</label>
|
</label>
|
||||||
|
<label v-if="csvMapping.all_expenses" class="fc-label">
|
||||||
|
Data de pagamento (vencimento)
|
||||||
|
<input type="date" v-model="csvMapping.payment_date" class="fc-input" />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</NeonPanel>
|
</NeonPanel>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user