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)mostratotalExpected = 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)
Scenario: Registrar o gasto real do mês
Section titled “Scenario: Registrar o gasto real do mês”- 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)éundefinede 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)mostratotalOverspend = R$ 320etotalUnderspend = R$ 0
Scenario: Evolução em vários meses
Section titled “Scenario: Evolução em vários meses”- 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 depoisMercado.recordSpend(julho/2026, R$ 1.200) - Then cada mês mantém seu próprio actual —
Mercado.actualFor(junho)é R$ 1.820 eMercado.actualFor(julho)é R$ 1.200 - And
Mercado.varianceFor(junho)é +R$ 320 (overspend) eMercado.varianceFor(julho)é −R$ 300 (underspend) - And
actualTotal(junho/2026)é R$ 4.320 eactualTotal(julho/2026)é R$ 3.700 — meses diferentes, totais diferentes - And o
varianceReport(julho/2026)mostratotalOverspend = R$ 0etotalUnderspend = 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)mostratotalActual = totalExpected = R$ 4.000(planning view, sem fatos)
Modelo
Section titled “Modelo”- Contexto existente —
budget/(sem nenhuma entity nova).RecurringExpenseganha tracking de actual por período;Budgetganha agregação por mês. - Renomeação —
RecurringExpense.valorviraRecurringExpense.expected. O nomevalorera ambíguo (planejado? real?) —expecteddeixa claro que é o orçado. O spec 000 precisa de touchup quando essa renomeação rodar (valor: Money→expected: Moneynoregistere no acessoexpense.valor→expense.expected). - Sem VO novo —
Period(já no shared-kernel) é a chave do map de actuals. Variance é sóMoneyassinado. - Storage do actual —
Map<periodKey, Money>privado dentro deRecurringExpense.recordSpendsobrescreve o registro daquele mês (não acumula — quem registrar duas vezes está corrigindo).
API do aggregate / entity
Section titled “API do aggregate / entity”// RecurringExpenseRecurringExpense.create({ ..., expected: Money }) // renomeia `valor` para `expected`recurringExpense.expected: Money // readonlyrecurringExpense.recordSpend(period: Period, actual: Money): void // sobrescreve registro daquele mêsrecurringExpense.actualFor(period: Period): Money | undefined // undefined = sem registrorecurringExpense.varianceFor(period: Period): Money | undefined // actual - expected; positivo = estourou, negativo = sobrou
// Budgetbudget.actualTotal(period: Period): Money // soma: para cada expense, usa actualFor(period) se existir; senão fallback para expectedbudget.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)}Decisões de design (UX-driven)
Section titled “Decisões de design (UX-driven)”- 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:
actualForretornaundefined,varianceForretornaundefined, masactualTotalcai 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/totalUnderspendchegam 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 emAccount.) - 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”. byExpenseno report só lista expenses com record: planning view (sem nenhum record) devolvebyExpensevazio, não um map de zeros.
Fora de escopo
Section titled “Fora de escopo”- 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 expense —
expectedeactualvivem na currency doBudget. 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 aoRecurringExpense.
Próximo passo
Section titled “Próximo passo”Renomear valor → expected no RecurringExpense, propagar pro Budget e pro spec 000, e implementar recordSpend/actualFor/varianceFor + actualTotal/varianceReport até os 4 scenarios passarem.