Skip to content

002 — Meta de poupança do casal (multi-currency, ritmo, previsão)

A gente define uma meta financeira do casal (viagem, reserva, casa, troca de carro…) com valor-alvo e prazo. Cada um aporta ao longo do tempo, podendo ser em moedas diferentes. O sistema responde, a qualquer momento:

  • quanto cada um já pôs (por member),
  • quanto tem em cada moeda (multi-currency, sem conversão),
  • quanto precisa por mês pra bater a meta no prazo,
  • em que ritmo a gente está indo (pace),
  • se está no ritmo certo (onTrack),
  • previsão de quando vai bater mantendo o ritmo atual (forecast).

Modelo genérico no shape, couple-focused no conteúdo (toda contribuição tem autor).

Scenario: Criar uma meta e ver o ritmo zerado

Section titled “Scenario: Criar uma meta e ver o ritmo zerado”
  • Given estamos em 01/06/2026 e nenhuma meta cadastrada
  • When a gente cria a meta “Amsterdam Setembro/2026”, target €5.000, startedOn = 01/06/2026, deadline 01/09/2026, com Gabriel e esposa como members
  • Then a meta fica registrada com 0 poupado por member e 0 poupado por moeda
  • And o aporte mensal necessário é €5.000 / 3 meses = €1.666,67 (jun, jul, ago)
  • And o ritmo atual é €0/mês e a meta não está no ritmo certo
  • And a previsão de conclusão é indefinida (ritmo zero não chega)
  • And a meta ainda não está atingida

Scenario: Contribuir ao longo do tempo e ver evolução do ritmo

Section titled “Scenario: Contribuir ao longo do tempo e ver evolução do ritmo”
  • Given a meta “Amsterdam Setembro/2026” criada em 01/06/2026
  • When Gabriel aporta R$ 2.000 em 15/06, esposa aporta R$ 3.000 em 10/07, e Gabriel aporta €500 em 30/07
  • Then o progresso por member mostra: Gabriel = R$ 2.000 + €500, esposa = R$ 3.000
  • And o progresso por moeda mostra: BRL = R$ 5.000, EUR = €500
  • And consultando em 01/08/2026, o aporte mensal necessário em EUR cai pra (€5.000 - €500) / 1 mês restante = €4.500
  • And o ritmo na moeda-alvo (EUR) é €500 / 2 meses elapsed = €250/mês
  • And a meta não está no ritmo certo (pace €250 < required €4.500)
  • And mantendo esse ritmo, a previsão de conclusão é 01/02/2028 (hoje + remaining/pace em meses)
  • And a meta ainda não está atingida (saved em EUR < target em EUR; BRL não converte automaticamente)
  • Given a meta “Amsterdam Setembro/2026” com Gabriel já tendo aportado €500 em 30/07
  • When esposa aporta €4.500 em 20/08/2026
  • Then a meta fica marcada como atingida (isReached = true)
  • And o saldo em EUR bate exatamente o target (€5.000)
  • And o aporte mensal necessário em EUR vira zero
  • And a meta está no ritmo certo
  • And a previsão de conclusão é a data do aporte que bateu a meta (20/08/2026)
  • Context novogoals/ (aggregate root Goal).
  • Aggregate RootGoal (nome, target: Money, deadline: Date, members: Member[], lista de Contribution). Toda métrica é derivada, não armazenada.
  • Value ObjectsMember (só name), Contribution (amount: Money, date: Date, paidBy: Member, source?: string, note?: string).
  • Shared Kernel reusadoMoney, Period.
Goal.create({ nome, target: Money, startedOn: Date, deadline: Date, members: Member[] }): Goal
goal.contribute({ amount: Money, date: Date, paidBy: Member, source?: string, note?: string }): void
// estado
goal.savedByCurrency(): Map<string, Money> // currency code → total
goal.savedBy(member: Member): Map<string, Money> // por moeda, pode ter múltiplas
goal.savedInTarget(): Money // só na currency do target
goal.remaining(): Money // target - savedInTarget
goal.isReached(): boolean // savedInTarget >= target
// ritmo
goal.requiredMonthly(today: Date): Money // na currency do target; zero se reached
goal.pace(today: Date): Money // média mensal na currency do target
goal.onTrack(today: Date): boolean // pace >= requiredMonthly OR reached
goal.forecast(today: Date): Date | null // null se pace = 0; data do reached se já atingiu
  • Domain não converte moeda. UX converte. progress, pace, forecast, requiredMonthly operam só na moeda do target. Outras moedas existem em savedByCurrency (transparência) mas não entram no cálculo de ritmo nem de “reached”. UX (ou cenário futuro com câmbio) pega savedByCurrency e converte com taxa do dia se quiser mostrar projeção.
    • Por quê: evita decisão de câmbio escondida no domínio. Câmbio é decisão de momento, não de modelo.
  • Atribuição obrigatória: toda contribute exige paidBy: Member. Sem isso, “meta do casal” é só “meta com nome em PT”.
  • Métricas de ritmo são derivadas, não armazenadas: pace, onTrack, forecast recalculam a cada chamada. Sempre verdade do momento.
  • forecast = "indefinido" quando pace = 0 (ninguém aportou na moeda-alvo). UX traduz pra mensagem motivacional (“comece com um aporte na moeda da meta”).
  • Sem sub-metas, sem split de contribuição, sem recurring contribution. YAGNI até cenário pedir.
  • source e note livres (string?). Não viram VO até aparecer regra de negócio.
  • Câmbio / conversão de moeda (FX rate como input em progress) → cenário futuro. Hoje UX converte por fora se precisar.
  • Custo variável (expected vs actual) → cenário 003.
  • Reconciliação fatura → expense → cenário 004.
  • Income + feasibility da meta (“dado que sobra R$ X/mês, essa meta é viável?”) → cenário 005.
  • Sugestão de split entre members (“Gabriel pôs menos, sugiro X dele”) → cenário futuro, com regra explícita.
  • Sub-itens da viagem (passagem, hotel, comida) — quando aparecer, vira Trip num context próprio, não Goal.
  • Estado archived / cancelled.
  • Member como Entity/Aggregate.
  • Deletar/editar contribuição (corrigir aporte errado = nova contribuição compensatória).

Implementar context goals/ (aggregate Goal, VOs Member e Contribution) até os 3 scenarios passarem. Sem repositório, sem persistência — coleção em memória dentro do aggregate.