Skip to content

003 — Custo variável (expected vs actual)

Nem todo custo da casa é fixo. Aluguel é fixo: orçado R$ 2.500, pago R$ 2.500 todo mês. Mercado varia: orçamos R$ 1.500 por mês, mas em junho a gente gastou R$ 1.820 e em julho R$ 1.200. O orçamento precisa diferenciar o que a gente planejou gastar (expected) do que a gente realmente gastou (actual), por mês.

A entrada do gasto real é manual: a gente chama recordSpend(period, actual) no fim do mês (ou quando quiser). Sem fatura, sem reconciliação automática — isso é cenário 004.

A partir disso, o orçamento responde, para qualquer mês:

  • quanto a gente realmente gastou (somando actual quando existe, expected como fallback),
  • quanto estourou ou sobrou em cada conta (variance assinada por expense),
  • o total estourado e o total economizado no mês (variance report).

Scenario: Custo só orçado vê apenas o expected

Section titled “Scenario: Custo só orçado vê apenas o expected”
  • Given um orçamento da casa com Mercado (expected R$ 1.500) e Aluguel (expected R$ 2.500), nenhum gasto real registrado
  • When a gente pergunta o total realmente gasto em junho/2026
  • Then o total vem R$ 4.000 (fallback puro a expected — sem record, planning view)
  • And o varianceReport(junho/2026) mostra totalExpected = R$ 4.000, totalActual = R$ 4.000, totalOverspend = R$ 0, totalUnderspend = R$ 0
  • And cada expense aparece com varianceFor(junho) = undefined (sem registro = sem variance)
  • Given o orçamento da casa com Mercado (expected R$ 1.500) e Aluguel (expected R$ 2.500)
  • When a gente registra Mercado.recordSpend(junho/2026, R$ 1.820)
  • Then Mercado.actualFor(junho/2026) é R$ 1.820
  • And Mercado.varianceFor(junho/2026) é +R$ 320 (estourou)
  • And Aluguel continua sem record — Aluguel.actualFor(junho/2026) é undefined e segue caindo no fallback
  • And o actualTotal(junho/2026) é R$ 1.820 (mercado real) + R$ 2.500 (aluguel fallback) = R$ 4.320
  • And o varianceReport(junho/2026) mostra totalOverspend = R$ 320 e totalUnderspend = R$ 0
  • Given o orçamento da casa com Mercado (expected R$ 1.500) e Aluguel (expected R$ 2.500)
  • When a gente registra Mercado.recordSpend(junho/2026, R$ 1.820) e depois Mercado.recordSpend(julho/2026, R$ 1.200)
  • Then cada mês mantém seu próprio actual — Mercado.actualFor(junho) é R$ 1.820 e Mercado.actualFor(julho) é R$ 1.200
  • And Mercado.varianceFor(junho) é +R$ 320 (overspend) e Mercado.varianceFor(julho) é −R$ 300 (underspend)
  • And actualTotal(junho/2026) é R$ 4.320 e actualTotal(julho/2026) é R$ 3.700 — meses diferentes, totais diferentes
  • And o varianceReport(julho/2026) mostra totalOverspend = R$ 0 e totalUnderspend = R$ 300

Scenario: Mês futuro sem registro volta a ser planning view

Section titled “Scenario: Mês futuro sem registro volta a ser planning view”
  • Given o orçamento da casa com Mercado (expected R$ 1.500) e Aluguel (expected R$ 2.500), com Mercado já tendo gasto real em junho/2026
  • When a gente pergunta o actualTotal(setembro/2026) antes de qualquer recordSpend daquele mês
  • Then o total vem R$ 4.000 — soma pura dos expected, igual ao mês zerado
  • And o varianceReport(setembro/2026) mostra totalActual = totalExpected = R$ 4.000 (planning view, sem fatos)
  • Contexto existentebudget/ (sem nenhuma entity nova). RecurringExpense ganha tracking de actual por período; Budget ganha agregação por mês.
  • RenomeaçãoRecurringExpense.valor vira RecurringExpense.expected. O nome valor era ambíguo (planejado? real?) — expected deixa claro que é o orçado. O spec 000 precisa de touchup quando essa renomeação rodar (valor: Moneyexpected: Money no register e no acesso expense.valorexpense.expected).
  • Sem VO novoPeriod (já no shared-kernel) é a chave do map de actuals. Variance é só Money assinado.
  • Storage do actualMap<periodKey, Money> privado dentro de RecurringExpense. recordSpend sobrescreve o registro daquele mês (não acumula — quem registrar duas vezes está corrigindo).
// RecurringExpense
RecurringExpense.create({ ..., expected: Money }) // renomeia `valor` para `expected`
recurringExpense.expected: Money // readonly
recurringExpense.recordSpend(period: Period, actual: Money): void // sobrescreve registro daquele mês
recurringExpense.actualFor(period: Period): Money | undefined // undefined = sem registro
recurringExpense.varianceFor(period: Period): Money | undefined // actual - expected; positivo = estourou, negativo = sobrou
// Budget
budget.actualTotal(period: Period): Money
// soma: para cada expense, usa actualFor(period) se existir; senão fallback para expected
budget.varianceReport(period: Period): {
totalExpected: Money,
totalActual: Money,
totalOverspend: Money, // soma das variances positivas
totalUnderspend: Money, // soma das variances negativas em valor absoluto
byExpense: Map<string, Money> // expenseId → variance assinada (presente só para expenses com record naquele mês)
}
  • Manual entry, não inferência: o casal digita recordSpend(junho, R$ 1.820). Cenário 004 vai puxar isso automaticamente da fatura — hoje fica manual pra a UX já existir.
  • Sem record = planning view, não erro: actualFor retorna undefined, varianceFor retorna undefined, mas actualTotal cai no expected. A gente nunca trava a leitura por falta de fato — mostra a melhor estimativa.
  • Variance assinada: positivo = estourou, negativo = sobrou. UX traduz pro humano (vermelho/verde). totalOverspend/totalUnderspend chegam pré-quebrados pro report não precisar reduzir de novo.
  • Sobrescreve, não acumula: chamar recordSpend(junho, R$ 1.820) duas vezes mantém o último. Correção de digitação > soma cumulativa. (Acumulação seria fatura+fatura, e isso vive em Account.)
  • Variance só existe se houver actual: varianceFor(junho) sem record é undefined, não zero. Zero significaria “gastei exatamente o orçado” — informação diferente de “ainda não medi”.
  • byExpense no report só lista expenses com record: planning view (sem nenhum record) devolve byExpense vazio, não um map de zeros.
  • Reconciliação fatura → expense (puxar actual automaticamente da fatura do cartão) → cenário 004.
  • Income / receita do casal → cenário 005.
  • Viabilidade de meta (“com esse padrão de gasto, a meta X bate?”) → cenário 006.
  • Histórico acumulado de variance / médias móveis (“em média você estoura R$ 200 no mercado”) — quando aparecer, vira analytics.
  • Múltiplas moedas por expenseexpected e actual vivem na currency do Budget. Câmbio fora.
  • Edição/remoção de record — corrigir = sobrescrever; cancelar = chamar com Money.zero (manual). Quando virar regra real, abre cenário.
  • Variance no nível de transação (linha-a-linha da fatura) — granularidade pertence ao Invoice, não ao RecurringExpense.

Renomear valorexpected no RecurringExpense, propagar pro Budget e pro spec 000, e implementar recordSpend/actualFor/varianceFor + actualTotal/varianceReport até os 4 scenarios passarem.