010 — Persistir o resto (Goal, Household, Account/Invoice sobrevivem ao reload)
O cenário 007 cobriu Budget. Agora o resto precisa do mesmo tratamento: a meta de Amsterdam, a renda do household, as faturas importadas — tudo tem que sobreviver quando o casal fecha e reabre o app.
A história é a mesma do 007, replicada pra três aggregates: salvo, fecho o app, abro de novo, tudo continua. O como (SQLite via drizzle, schemas próprios por context) é decisão técnica (ADR 001) e a mecânica de mapeamento row ↔ aggregate vive em specs colocadas em cada infrastructure/ (ADR 003). Este cenário cuida do contrato comportamental dos três repositories — testado contra fakes InMemory no tier domain (ADR 002).
O pattern é simétrico ao 007:
<Aggregate>Repository= port em<ctx>/application/. Interface enxuta:save(aggregate),load(id),list().InMemory<Aggregate>Repository= adapter test fake em<ctx>/infrastructure/. Map por id.Sqlite<Aggregate>Repository= adapter de produção em<ctx>/infrastructure/. Spec colocada cobre mapeamento real contra:memory:.<Aggregate>.id= promovido a campo estável (UUID default, overload externo pra reconstrução pelo adapter). Análogo aoBudget.iddo 007 — extension natural, sem flag de “modo persistido”.
Member já tem id estável desde o 005, então Household e Goal herdam identidade dos members reusando as mesmas instâncias (mesma referência) ou via reconstrução por id na hidratação do adapter — fica a critério da implementação, desde que equals continue passando após reload.
Scenario: Salvar e recarregar a meta de Amsterdam preserva contribuições e ritmo
Section titled “Scenario: Salvar e recarregar a meta de Amsterdam preserva contribuições e ritmo”- Given a meta “Amsterdam Setembro/2026” criada em 01/06/2026 com target €5.000, deadline 01/09/2026, members Gabriel e esposa
- And Gabriel aportou R$ 2.000 em 15/06, esposa aportou R$ 3.000 em 10/07 e Gabriel aportou €500 em 30/07
- And um
InMemoryGoalRepository - When a gente chama
repo.save(meta) - And depois
repo.load(meta.id)(simulando fechar e reabrir o app) - Then a meta recarregada tem o mesmo
nome,target,startedOn,deadline - And
savedByCurrency()mostra BRL R$ 5.000 + EUR €500 - And
savedBy(gabriel)mostra BRL R$ 2.000 + EUR €500 (Gabriel reusado por instância/id) - And
savedBy(esposa)mostra BRL R$ 3.000 - And
savedInTarget()continua €500 eremaining()€4.500 - And
requiredMonthly(01/08/2026)continua €4.500 (consistente com 002) - And
pace(01/08/2026)continua €250/mês eonTrack(01/08/2026)continuafalse
Scenario: Salvar e recarregar o household preserva renda e members
Section titled “Scenario: Salvar e recarregar o household preserva renda e members”- Given o household “Casa” em BRL com Gabriel (R$ 8.000) e esposa (R$ 6.000) atribuídos
- And um
InMemoryHouseholdRepository - When a gente chama
repo.save(casa) - And depois
repo.load(casa.id) - Then o household recarregado tem
nome“Casa” ecurrencyBRL - And
members().lengthé 2 (Gabriel e esposa, ids estáveis preservados via sharedMember) - And
monthlyIncome()continua R$ 14.000 - And
incomeBy(gabriel)é R$ 8.000 eincomeBy(esposa)é R$ 6.000 - And depois de
assignIncome(gabriel, R$ 9.000)+ novosave+load,monthlyIncome()vira R$ 15.000 e o reload não duplica linha de income
Scenario: Salvar e recarregar o cartão Nubank preserva faturas e status paid
Section titled “Scenario: Salvar e recarregar o cartão Nubank preserva faturas e status paid”- Given o cartão Nubank (BRL, fechamento dia 25, vencimento dia 10) com a fatura de maio/2026 (3 transações somando R$ 4.500) importada
- And a fatura de junho/2026 (2 transações somando R$ 1.000) também importada
- And a fatura de maio marcada como paga em 10/06/2026
- And um
InMemoryAccountRepository - When a gente chama
repo.save(cartao) - And depois
repo.load(cartao.id) - Then o cartão recarregado tem o mesmo
nome,cycle,currency - And
invoices()retorna duas faturas, ordenadas (maio antes de junho) - And cada fatura preserva
period,closedAt,dueDate,transactions()(comdescriptioneamount) etotal() - And a fatura de maio continua
isPaid() === trueeisOverdue(11/06/2026)continuafalse - And a fatura de junho continua
isPaid() === false
Modelo
Section titled “Modelo”Três ports espelhando o BudgetRepository do 007:
interface GoalRepository { save(goal: Goal): Promise<void> | void; load(id: string): Promise<Goal | undefined> | Goal | undefined; list(): Promise<Goal[]> | Goal[];}
// src/contexts/household/application/HouseholdRepository.tsinterface HouseholdRepository { save(household: Household): Promise<void> | void; load(id: string): Promise<Household | undefined> | Household | undefined; list(): Promise<Household[]> | Household[];}
// src/contexts/accounts/application/AccountRepository.tsinterface AccountRepository { save(account: Account): Promise<void> | void; load(id: string): Promise<Account | undefined> | Account | undefined; list(): Promise<Account[]> | Account[];}- Identidade dos aggregates —
Goal,HouseholdeAccountganhamidestável (UUID default viacrypto.randomUUID(), overload externo pra reconstrução). Análogo aoBudget.iddo 007. - Member compartilhado entre contexts —
Member.id(estável desde 005) é a chave queGoaleHouseholdpersistem; a hidratação reconstróiMembercom o mesmo id ou aceita uma lookup table externa. Como o spec aqui é tier domain (fake), basta reusar a instância — a mecânica real cabe no adapter SQLite. - Adapter test fake —
Map<id, Aggregate>. Implementação espelhaInMemoryBudgetRepository(cenário 007). - Adapter de produção — schema próprio por context (
goals/contributions,households/incomes,accounts/invoices/transactions). Mecânica em specs colocadas (ADR 003).
Decisões de design
Section titled “Decisões de design”- Port em
application/, fake e sqlite eminfrastructure/. Mesmo lugar do 007. Domain não importa de application nem infrastructure. - Aggregates ganham
idpor extension natural.Goal.create({...}),Household.create({...}),Account.openCreditCard({...})continuam funcionando —idvem por UUID default, specs antigos (001, 002, 005) não precisam de touchup. Overload{ id, ... }é nova, opcional, usada só pelo adapter na reconstrução. - Contrato testado contra fake; driver testado contra real. Mesma divisão do 007. Tier domain prova que o contrato independe do driver. Tier infra (specs colocadas) prova que o SQLite implementa o contrato.
- Sem persistência cross-aggregate atômica. Cada repo é independente — não tem
UnitOfWorkou transaction wrapper. Se aparecer caso (“salvarGoaleHouseholdjuntos atomicamente”), entra cenário separado. - Member reusado por referência no spec domain. Mesma gotcha do 002/005: o spec cria
const gabriel = Member.create({...})uma vez e reusa a instância noaddMember,assignIncome,contribute({paidBy: gabriel}).equalspor id continua valendo após reload desde que o adapter rehidrate com o mesmo id (fake guarda a instância; SQLite reconstrói).
Impacto em outros cenários
Section titled “Impacto em outros cenários”- Cenários 001, 002, 005 —
Account.openCreditCard({...}),Goal.create({...}),Household.create({...})continuam funcionando. Oidé gerado automaticamente; specs antigos não precisam de touchup. As overloads{ id, ... }são novas e opcionais. - Cenário 003 (Goal.contribute com data variada) — round-trip de contribuições com diferentes moedas e datas é exercitado neste cenário (Amsterdam) e também na spec colocada do
SqliteGoalRepository. - Cenário 001 (Invoice) — round-trip de
markPaidé exercitado aqui (fatura de maio paga sobrevive); detalhamento de transações fica na spec colocada doSqliteAccountRepository.
Fora de escopo
Section titled “Fora de escopo”- Migrations / schema versioning.
- Transactions cross-aggregate atômicas.
- Soft-delete, audit trail, histórico de versões.
- Concorrência / locking — local-first, single-user.
- Cache em camada de aplicação.
- Performance / index.
- Repository pra
RecurringExpensestandalone (parte doBudget, não tem identidade fora dele) — coberto pelo 007. - Repository pra
Contribution,Invoice,Transactionstandalone (parte do aggregate respectivo). - Repository pra
Memberstandalone —Membermora emshared-kernel/; persistência hoje é via aggregate dono (Household,Goal). Quando aparecer “diretório de members” entra cenário próprio.
Próximo passo
Section titled “Próximo passo”Implementar três ports + três fakes + três adapters SQLite (+ schemas drizzle) até este spec passar (tier domain) e as três specs colocadas em src/contexts/{goals,household,accounts}/infrastructure/ passarem contra :memory: (tier infra).