Skip to content

004 — Reconciliar fatura do cartão com os custos do orçamento

Toda fatura do cartão importada (cenário 001) é cheia de transações que, na real, são instâncias dos custos recorrentes do orçamento (cenário 000) — Netflix vira RecurringExpense “Netflix”, compra no mercado vira a linha “Mercado”, e assim por diante. A gente quer que o sistema categorize automaticamente essas transações no RecurringExpense correspondente, alimentando o actual do mês (cenário 003), sem a gente precisar lançar gasto a gasto na mão.

Matching é simples e previsível: cada RecurringExpense declara uma lista de aliases (substrings) e o reconciler casa por substring case-insensitive entre transaction.description e os aliases — primeiro expense que casa, ganha.

  • Given um orçamento da casa com três custos: Netflix (R$ 120, aliases ["netflix"]), Mercado (R$ 1.500, aliases ["mercado livre", "carrefour"]) e Aluguel (R$ 2.500, sem aliases)
  • And uma fatura do cartão de junho/2026 com três transações: “NETFLIX.COM” R$ 120, “MERCADO LIVRE” R$ 380, “LATAM AIRLINES” R$ 4.000
  • When a gente roda ExpenseReconciler.applyInvoice(orcamento, fatura)
  • Then a transação da Netflix casa com o expense Netflix (case-insensitive, “netflix” ⊂ “NETFLIX.COM”) e o actualFor(junho/2026) da Netflix passa a ser R$ 120
  • And a transação do Mercado Livre casa com o expense Mercado (primeiro alias bate) e o actualFor(junho/2026) do Mercado passa a ser R$ 380
  • And a transação da LATAM fica em unmatched (nenhum alias bate)
  • And o Aluguel não casa nada (não tem alias e não tem transação correspondente na fatura), o que é OK — aluguel não vem no cartão
  • And totalCategorized é R$ 500 (R$ 120 + R$ 380)

Scenario: Múltiplas transações casam o mesmo expense e somam no actual

Section titled “Scenario: Múltiplas transações casam o mesmo expense e somam no actual”
  • Given o mesmo orçamento da casa
  • And uma fatura de junho/2026 com três transações que caem no Mercado: “MERCADO LIVRE” R$ 200, “CARREFOUR” R$ 180, “MERCADO LIVRE” R$ 50
  • When a gente reconcilia a fatura
  • Then as três transações aparecem em matched, todas apontando pro expense Mercado
  • And actualFor(junho/2026) do Mercado é R$ 430 (soma incremental: 200 + 180 + 50)
  • And totalCategorized é R$ 430

Scenario: Faturas em meses diferentes alimentam períodos independentes

Section titled “Scenario: Faturas em meses diferentes alimentam períodos independentes”
  • Given o mesmo orçamento da casa
  • And a fatura de junho/2026 com “MERCADO LIVRE” R$ 380
  • And a fatura de julho/2026 com “MERCADO LIVRE” R$ 520
  • When a gente reconcilia as duas faturas
  • Then actualFor(junho/2026) do Mercado é R$ 380
  • And actualFor(julho/2026) do Mercado é R$ 520
  • And os dois períodos são independentes (junho não vaza pra julho nem vice-versa)
  • Domain Service novoExpenseReconciler em budget/domain/services/. Lógica pura, sem I/O, sem repositório. Lê Invoice do context accounts e escreve em RecurringExpense do context budget.
  • RecurringExpense ganha:
    • aliases: readonly string[] (opcional na criação; sem aliases = não reconcilia automaticamente).
    • matches(description: string): boolean — true se algum alias está contido em description.toLowerCase().
    • addToActual(period: Period, money: Money): void — incrementa o actual do período (soma quando já existe, parte do zero quando não).
  • ReconciliationResult:
    interface ReconciliationResult {
    matched: Array<{ transaction: Transaction; expense: RecurringExpense }>;
    unmatched: Transaction[];
    totalCategorized: Money;
    }
RecurringExpense.create({ ..., aliases?: string[] })
recurringExpense.aliases: readonly string[]
recurringExpense.matches(description: string): boolean
recurringExpense.addToActual(period: Period, money: Money): void
ExpenseReconciler.applyInvoice(budget: Budget, invoice: Invoice): ReconciliationResult
  • First-match substring case-insensitive. Pra cada transação, varre os expenses do Budget na ordem de registro e pega o primeiro cujo matches(description) retorna true. Múltiplos aliases por expense é OR (qualquer um bate). Se dois expenses casariam a mesma transação, vence quem foi registrado primeiro — sem warning, sem conflito explícito. Por quê: ordem de registro é determinística e suficiente; quando aparecer conflito real, a UX edita aliases.
  • Sem fuzzy/regex/LLM matching. Substring case-insensitive é trivial de explicar, debugar e ajustar manualmente. Fuzzy match (Levenshtein, regex, LLM) é cenário futuro — só entra se a gente sentir falta na prática.
  • ExpenseReconciler é Domain Service, não Application Service. Lógica é pura (Budget + Invoice → resultado), sem I/O nem repositório. Mora em budget/domain/services/ porque escreve no Budget; lê o Invoice como input read-only.
  • Cross-context read-only de Budget pra Accounts. O reconciler importa Invoice/Transaction do context accounts na direção domain service → entities. Sem inversão de dependência (interface/port) por enquanto — quando aparecer um segundo consumidor desse tipo de leitura, aí refatora.
  • addToActual é incremental por design. Múltiplas transações da mesma fatura caem no mesmo expense (Mercado Livre + Carrefour + outra compra) — somar é a operação natural. recordSpend (sobrescreve, do cenário 003) continua existindo pra entrada manual de mês fechado.
  • aliases opcional. Sem aliases = expense não reconcilia automaticamente (caso do Aluguel, que não vem no cartão). Não é regra estranha, é a default natural: se você não declarou como casar, não casa.
  • Fuzzy match, regex, Levenshtein, LLM-based matching → cenário futuro se substring não der conta.
  • Editar manualmente uma transação categorizada errada (mover transação X do expense A pro expense B) → cenário futuro.
  • Undo / desfazer reconciliação (reverter addToActual) → cenário futuro.
  • Conflitos quando vários expenses casam a mesma transação — resolvido por first-match, sem alertar; quando virar problema na prática, a gente faz UI de revisão.
  • Reconciliar várias faturas na mesma chamada (applyInvoices(budget, invoices[])) — uma por vez por enquanto; loop no caller resolve.
  • Income / receita (creditar entradas no orçamento) → cenário futuro.
  • Re-reconciliação idempotente (rodar applyInvoice duas vezes na mesma fatura não duplica) — happy path assume uma única chamada por fatura.

Adicionar aliases/matches/addToActual a RecurringExpense, criar budget/domain/services/ExpenseReconciler.ts e exportar pelo barrel até os três scenarios do spec passarem.