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/paceem meses) - And a meta ainda não está atingida (saved em EUR < target em EUR; BRL não converte automaticamente)
Scenario: Atingir a meta na moeda-alvo
Section titled “Scenario: Atingir a meta na moeda-alvo”- 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)
Modelo
Section titled “Modelo”- Context novo —
goals/(aggregate rootGoal). - Aggregate Root —
Goal(nome,target: Money,deadline: Date,members: Member[], lista deContribution). Toda métrica é derivada, não armazenada. - Value Objects —
Member(sóname),Contribution(amount: Money,date: Date,paidBy: Member,source?: string,note?: string). - Shared Kernel reusado —
Money,Period.
API do aggregate
Section titled “API do aggregate”Goal.create({ nome, target: Money, startedOn: Date, deadline: Date, members: Member[] }): Goalgoal.contribute({ amount: Money, date: Date, paidBy: Member, source?: string, note?: string }): void
// estadogoal.savedByCurrency(): Map<string, Money> // currency code → totalgoal.savedBy(member: Member): Map<string, Money> // por moeda, pode ter múltiplasgoal.savedInTarget(): Money // só na currency do targetgoal.remaining(): Money // target - savedInTargetgoal.isReached(): boolean // savedInTarget >= target
// ritmogoal.requiredMonthly(today: Date): Money // na currency do target; zero se reachedgoal.pace(today: Date): Money // média mensal na currency do targetgoal.onTrack(today: Date): boolean // pace >= requiredMonthly OR reachedgoal.forecast(today: Date): Date | null // null se pace = 0; data do reached se já atingiuDecisões de design (UX-driven)
Section titled “Decisões de design (UX-driven)”- Domain não converte moeda. UX converte.
progress,pace,forecast,requiredMonthlyoperam só na moeda do target. Outras moedas existem emsavedByCurrency(transparência) mas não entram no cálculo de ritmo nem de “reached”. UX (ou cenário futuro com câmbio) pegasavedByCurrencye 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
contributeexigepaidBy: Member. Sem isso, “meta do casal” é só “meta com nome em PT”. - Métricas de ritmo são derivadas, não armazenadas:
pace,onTrack,forecastrecalculam 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.
sourceenotelivres (string?). Não viram VO até aparecer regra de negócio.
Fora de escopo
Section titled “Fora de escopo”- 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
Tripnum context próprio, nãoGoal. - Estado
archived/cancelled. Membercomo Entity/Aggregate.- Deletar/editar contribuição (corrigir aporte errado = nova contribuição compensatória).
Próximo passo
Section titled “Próximo passo”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.