005 — Renda do casal (Household + income mensal por member)
A gente quer registrar quem mora junto e quanto cada um ganha por mês. Isso vira a base pra qualquer pergunta de viabilidade (“essa meta cabe?”, “esse orçamento cabe?”, “sobra quanto?”). Por enquanto só o essencial: o household tem nome, uma moeda, uma lista de members, e um income mensal por member. O income total é a soma.
- household = a unidade de convivência financeira (o casal, eventualmente com dependentes).
- member = pessoa que faz parte do household. Promovido de VO (em
goals/) pra Entity emshared-kernel/porque agora vários contexts precisam apontar pro mesmo “Gabriel”. - income = quanto o member ganha por mês, na moeda do household.
Scenario: Criar household do casal com income de cada um
Section titled “Scenario: Criar household do casal com income de cada um”- Given nenhum household cadastrado
- When a gente cria o household “Casa” em BRL, adiciona Gabriel e esposa, e atribui income de R$ 8.000 pro Gabriel e R$ 6.000 pra esposa
- Then
monthlyIncome()= R$ 14.000 - And
incomeBy(gabriel)= R$ 8.000 eincomeBy(esposa)= R$ 6.000 - And
members()lista os dois
Scenario: Reajuste salarial
Section titled “Scenario: Reajuste salarial”- Given o household “Casa” com Gabriel em R$ 8.000 e esposa em R$ 6.000
- When Gabriel recebe reajuste e
assignIncomeé chamado de novo com R$ 9.000 - Then
monthlyIncome()= R$ 15.000 (não duplica) - And
incomeBy(gabriel)= R$ 9.000
Scenario: Member sem income atribuído
Section titled “Scenario: Member sem income atribuído”- Given o household “Casa” com Gabriel (R$ 9.000) e esposa (R$ 6.000)
- When a gente adiciona um terceiro member “dependente” sem chamar
assignIncomepra ele - Then
members().length= 3 - And
monthlyIncome()continua R$ 15.000 - And
incomeBy(dependente)=undefined
Modelo
Section titled “Modelo”- Context novo —
household/(aggregate rootHousehold, singular porque um casal tem um household principal). - Aggregate Root —
Household(nome,currency: string,members: Member[],incomes: Map<MemberId, Money>). - Entity migrada —
Membersai degoals/(era VO{ name }) e vira Entity emshared-kernel/comidestável (UUID viacrypto.randomUUID()). Identidade porid, não porname. - Shared Kernel reusado —
Money.
API do aggregate
Section titled “API do aggregate”Member.create({ name: string }): Member // id gerado via crypto.randomUUID()member.id: string // readonly, estávelmember.name: string // readonlymember.equals(other: Member): boolean // por id
// contexts/household/domain/Household.tsHousehold.create({ nome: string, currency: string }): Householdhousehold.nome: stringhousehold.currency: stringhousehold.addMember(member: Member): voidhousehold.assignIncome(member: Member, monthly: Money): void // substitui se já existirhousehold.monthlyIncome(): Money // soma; zero se ninguém tem incomehousehold.incomeBy(member: Member): Money | undefined // undefined se member sem incomehousehold.members(): Member[]Decisões de design
Section titled “Decisões de design”- Member promove de VO → Entity porque ID estável vira essencial: persistência (SQLite planejado), cross-context (Goal aponta pro mesmo Gabriel que Household aponta), serialização. Identidade passa a ser
id, nãoname— assim “Gabriel” pode mudar de nome (apelido, sobrenome de casamento) sem virar pessoa diferente. - Member mora em
shared-kernel/, não emhousehold/. Múltiplos contexts precisam (goals hoje, household agora, budget e atribuição de despesa amanhã). Member não é dono do household — é compartilhado. - Single currency por Household — KISS. Casa em BRL, casa em EUR, ok. Casal expat com salários em currencies diferentes entra quando aparecer o caso. Hoje seria complexidade sem demanda.
- Income é só monthly — sem frequency, sem variable income, sem 13º. YAGNI até cenário pedir.
assignIncomesobrescreve, não acumula. Reajuste é a operação natural — chamar de novo com valor novo substitui. Histórico de salário não é deste cenário.- Métricas derivadas, não armazenadas —
monthlyIncome()recalcula a cada chamada. Consistente comGoal.pace,Budget.total,Invoice.total.
Impacto em outros cenários
Section titled “Impacto em outros cenários”- Cenário 002 (Meta do casal) vai precisar de touchup minor:
Member.of("Gabriel")viraMember.create({ name: "Gabriel" }), e o import muda de../src/contexts/goals/domainpra../src/shared-kernel. Goal continua usando Member como antes (a API de igualdade muda de “by name” pra “by id”, mas Goal só fazequals, então é transparente desde que os mesmosMemberinstances sejam reusados nospaidBy). Esse touchup não é deste scenario — fica registrado aqui só pra deixar claro que a migração tem follow-up.
Fora de escopo
Section titled “Fora de escopo”- Income variável, freelance, múltiplas fontes por member.
- Frequência ≠ monthly (semanal, anual, 13º).
- Taxes, income líquido vs bruto.
- Dependents econômicos com modelo próprio (quem traz vs quem consome).
- Joint income / split de despesa entre members.
- Multi-currency dentro do household.
- Histórico de income (reajustes anteriores).
- Member como Aggregate Root — continua Entity simples.
- Repository / persistência — coleção em memória dentro do aggregate.
- Update do context
goals/pra novo Member (touchup separado).
Próximo passo
Section titled “Próximo passo”Implementar context household/ (aggregate Household) + migração de Member pra shared-kernel/ até os 3 scenarios passarem. Depois, touchup no 002 pra usar o novo Member.