Onde
O predicado é "este gasto/transação de cartão está sob regime de fatura neste mês". Enumerado por conteúdo, não por caminho — são 5 sites errados, todos em app/, contra 6 sites corretos, todos em lib/.
Sites errados — decidem sem comparar o mês com faturaActiveFrom:
app/(app)/dashboard/page.tsx:54 — const isFaturaMode = !isCycleView && creditMode.creditMode === 'fatura'. É a definição-raiz; os três seguintes derivam dela.
app/(app)/dashboard/page.tsx:107-110 — fixedForPendency, guardado por faturaCtx && creditIdSet.size > 0 (e faturaCtx só existe quando isFaturaMode). Alimenta pendingFixed/unpaidFixedCount.
app/(app)/dashboard/page.tsx:204 — creditAccountIds={isFaturaMode ? faturaCtx?.creditAccountIds : undefined} no <FixedExpenseList>.
app/(app)/dashboard/page.tsx:222 — o mesmo ternário no <TransactionList>.
app/(app)/configuracao-mes/page.tsx:147-153 — <FixedExpenseList> renderizado sem creditAccountIds nenhum, em página que nem chama getUserCreditMode. Predicado ausente, não errado.
Sites corretos — todos comparam o mês:
lib/queries/dashboard.ts:24-28 (getCategoryGroupProgress) — referenceMonth >= faturaCtx.faturaActiveFrom
lib/queries/dashboard.ts:227-231 (getDashboardData) — idem
lib/queries/panorama.ts:81 e :105 (getAnnualOverview) — a comparação vai por linha, no SQL (lt(transactions.referenceMonth, faturaActiveFrom))
lib/queries/panorama.ts:251-252 (getAnnualExpensesByGroup) — t.referenceMonth >= faturaCtx!.faturaActiveFrom
lib/queries/fatura.ts:347 — closedIsPreActivation = faturaActiveFrom !== null && closedCycleMonth < faturaActiveFrom
lib/actions/fatura.ts:127 — createFaturaPayment recusa com cycle_before_activation
Placar no recorte que importa (quem decide o regime de um mês): 6 × 5, e os 5 são todos consumidores dos 6.
Evidência
getDashboardData publica a resposta certa e diz, em comentário, que a publica exatamente para o consumidor não reexpressar o filtro (lib/queries/dashboard.ts:268-272):
// Diz se budgetTransactions/budgetFixedExpenses excluem crédito em relação a
// transactions/fixedExpenses — é o mesmo predicado usado para montar os dois
// conjuntos acima, exposto para quem precisa saber *por que* podem divergir
// (ex: nota informativa no drill-down) sem reexpressar o filtro por conta.
creditFilteredFromBudget: shouldFilterCredit,
A page recebe esse booleano, usa em um lugar (:184, o CategoryGroupProgress) e, três linhas depois, reexpressa o filtro por conta própria — sem a metade que compara o mês.
O efeito no componente não é cosmético. components/dashboard/FixedExpenseList.tsx:96-110 troca o botão por um disco inerte quando isViaFatura:
{isViaFatura ? (
<div className="h-5 w-5 flex-shrink-0 rounded-full border border-border bg-bg-subtle" />
) : (
<button onClick={toggle} ... aria-label={e.paid ? 'Marcar como pendente' : 'Marcar como pago'}>
e suprime a DueBadge (:158-166), que é quem emite "Vencido" / "Vence hoje".
Cenário completo, mês pré-ativação. components/contas/CreditModeSection.tsx:78-81 fixa min={currentYearMonth()} no seletor e escreve o hint "Meses anteriores mantêm o comportamento atual." — ou seja, faturaActiveFrom é sempre >= o mês da ativação, e todo o histórico do usuário é pré-ativação. Ativou em jun/2026, navega para mar/2026 pela seta do MonthSelector (normalizeYearMonthParam, lib/utils/date.ts:64-68, aceita qualquer ano > 2000 — não há piso em faturaActiveFrom):
getDashboardData('2026-03-01', ctx) → isFaturaMonth false → o gasto fixo do cartão entra em totalExpenses de março. Correto, e é o que o roadmap promete.
- a page →
isFaturaMode true → o mesmo gasto renderiza com selo "via fatura", sem checkbox de pago e sem badge de vencimento.
O mesmo card, no mesmo render, afirma que a despesa é de março (no total) e que ela é de uma fatura que não existe (na linha). E o checkbox some de forma permanente para todo mês anterior à ativação — o usuário não consegue mais marcar como pago um fixo que ele de fato pagou.
pendingFixed (site 2) cai junto: o banner de pendências deixa de contar gastos fixos de crédito em meses onde o próprio roadmap manda contá-los.
Cenário do site 5, mês sob fatura de verdade. Em /configuracao-mes, com o regime ativo e o mês >= faturaActiveFrom, o gasto fixo do cartão aparece com o checkbox clicável e com badge "Vencido" em vermelho a partir do dia seguinte ao dueDay — todo mês, para sempre, porque ninguém nunca o marca (o dashboard não deixa). A mesma linha, no dashboard, diz "via fatura" e não vence. Os dois sites de render de FixedExpenseList no repo são exatamente esses dois, e eles discordam.
docs/regime-fatura/05-roadmap.md, Fase 5, tem o passo marcado como concluído:
e o critério de aceite:
- Meses anteriores ao
faturaActiveFrom: comportamento accrual preservado.
A page implementou o passo dropando o qualificador "em meses de fatura". E o "Inventário de Arquivos → Alterados" do mesmo roadmap não lista app/(app)/configuracao-mes/page.tsx — a página nunca entrou no escopo do regime.
Verificações feitas (tentativa de falsificar o achado)
- A comparação de mês é convenção do repo?
grep -rn "faturaActiveFrom" em lib/, app/, components/: todo site de lib/ que decide o regime de um mês compara. Os 5 de app/ não. Não é discussão de arquitetura — é a minoria fora da convenção da maioria.
- Panorama é contraexemplo? Parecia:
panorama.ts:53-56 e :186-189 definem isFaturaMode sem o mês. Fui ler o uso: a comparação está aplicada por linha, mais abaixo (:81, :105 no SQL; :251-252 em JS). Panorama acerta; o candidato "panorama também erra" morreu aqui.
- O usuário consegue mesmo chegar a um mês pré-ativação? Sim.
normalizeYearMonthParam só rejeita ano <= 2000; não há piso em faturaActiveFrom, e o MonthSelector navega livremente. E min={currentYearMonth()} no seletor de ativação garante que o conjunto pré-ativação nunca é vazio.
- Foi decisão deliberada? Não deu para fechar por
git log -S: o clone é shallow (git rev-parse --is-shallow-repository → true, 237 commits, git blame devolve ^98c85f4 de 2026-07-31 como boundary). Sem histórico, a falsificação foi pelo artefato que sobrevive: o roadmap do domínio registra a decisão contrária em dois pontos (passo da Fase 5 e critério de aceite), e o hint da própria UI de ativação promete o comportamento que a page quebra. Não é escolha registrada em lugar nenhum — é o qualificador que se perdeu ao descer de lib/ para app/.
toggleFixedExpensePaid tem efeito financeiro? Não — grep por leituras de .paid devolve só render (FixedExpenseList, CategoryGroupProgress:80) e a contagem de pendências. O impacto do site 5 é de UI e de alarme falso, não de saldo. Registrado para o implementador não procurar um bug de número que não existe.
TransactionList sofre o mesmo? Sim, mas só cosmeticamente: TransactionList.tsx:168-172 usa isViaFatura apenas para o selo. Listado como site 4 para a correção fechar por inteiro, não como impacto próprio.
Impacto
Usuário em regime de fatura, ao revisar qualquer mês anterior à ativação (que é todo o histórico dele, pela regra do min): o dashboard soma a compra de cartão no total do mês e simultaneamente marca a linha como "via fatura"; o checkbox de "pago" do gasto fixo desaparece e não há como marcá-lo; o banner de pendências deixa de avisar sobre esses fixos. Contradiz o que a tela de ativação prometeu por escrito.
Usuário em regime de fatura, em mês corrente: /configuracao-mes acusa "Vencido" em vermelho, todo mês, nos gastos fixos do cartão que o dashboard trata como cobertos pela fatura — e oferece ali o toggle que o dashboard remove de propósito.
Usuário em accrual: zero diferença. faturaCtx é undefined e isFaturaMode é false nos dois casos.
Cobertura
O que já existe, e o que garante. __tests__/integration/queries-fatura.test.ts:338 — 'maio: totalExpenses inclui crédito pois referenceMonth < faturaActiveFrom' — é exatamente o cenário do achado, e passa. Ele protege a query, e vai continuar verde depois da correção: não toca a page. É a peça que torna o achado indefensável como "comportamento pretendido" — o repo já assere, em teste de integração, o oposto do que a page renderiza no mesmo mês. queries-dashboard.test.ts:342 e :414 asseram creditFilteredFromBudget (true em fatura, false em visão de ciclo); nenhum dos dois cobre o mês pré-ativação, e nenhum olha para app/.
O que falta. Nada assere o predicado da page. Sem @testing-library/react no projeto, o teste de render não é a via (instalar a infra seria maior que a correção) — vale o precedente de gate por asserção sobre fonte já versionado em __tests__/unit/paginas-publicas.test.ts e __tests__/unit/service-worker-registro.test.ts.
Proposta em duas partes, ambas vermelhas hoje:
- Unit test do helper novo, com a entrada que só a implementação certa rejeita:
expect(isFaturaMonth('2025-05-01', {
creditMode: 'fatura', faturaActiveFrom: '2025-06-01', creditAccountIds: ['x'],
})).toBe(false) // o predicado atual da page devolve true aqui
O par de controle ('2025-06-01' → true) não discrimina nada sozinho: a implementação errada também passa nele. Os dois juntos, sim.
- Gate de fonte no mesmo arquivo:
app/(app)/dashboard/page.tsx e app/(app)/configuracao-mes/page.tsx não podem conter creditMode === 'fatura' solto, e todo call site de <FixedExpenseList>/<TransactionList> tem de passar creditAccountIds. Sem essa segunda metade, alguém adiciona o helper, deixa a page como está, e a suíte fica verde sobre o furo.
Proposta
Parar de derivar o predicado na page. Duas correções distintas porque as duas pages têm dados distintos:
Dashboard (sites 1-4). Já tem a resposta pronta: trocar isFaturaMode por data.creditFilteredFromBudget nas linhas 107, 204 e 222. Ele é literalmente isFaturaMonth && creditIdSet.size > 0 (lib/queries/dashboard.ts:234), já é false na visão de ciclo (:403), e o comentário que o acompanha diz que existe para esse uso. isFaturaMode continua válido onde ele de fato significa "o modo do usuário, não o do mês": montar o faturaCtx (:56) e decidir se chama getOpenFaturas/getPaymentAccounts (:74-75). Não mexer nesses dois.
/configuracao-mes (site 5). Não chama getDashboardData, e chamá-la só para extrair um booleano puxaria quatro queries a mais. Precisa de getUserCreditMode + getCreditAccounts no Promise.all que já existe (:46-51) e do predicado compartilhado.
Helper, e por que esse. Exportar isFaturaMonth(referenceMonth: string, faturaCtx?: FaturaContext): boolean de lib/queries/fatura.ts, e reusá-lo nos dois sites de lib/queries/dashboard.ts que hoje o repetem inline.
- Não em
lib/utils/date.ts: é para lá que a mão vai primeiro, porque a expressão parece comparação de mês — mas o predicado também depende de creditMode e da existência de contas de crédito, que são estado de domínio, não aritmética de calendário. date.ts não conhece FaturaContext e não deveria passar a conhecer.
- Não
getFaturaState: responde sobre um ciclo de uma conta, faz I/O, e a page precisa de um booleano puro sobre um mês.
lib/queries/fatura.ts é onde FaturaContext já vive, e onde closedIsPreActivation (:347) já expressa a metade negativa do mesmo predicado.
Cuidado ao implementar: a correção deixa a suíte verde — nenhum teste existente muda de cor. O sinal de que funcionou são os dois casos novos da seção Cobertura.
Custo estimado
M — app/(app)/dashboard/page.tsx, app/(app)/configuracao-mes/page.tsx, lib/queries/fatura.ts (helper) e lib/queries/dashboard.ts (passa a reusar o helper), mais o arquivo de teste.
Onde
O predicado é "este gasto/transação de cartão está sob regime de fatura neste mês". Enumerado por conteúdo, não por caminho — são 5 sites errados, todos em
app/, contra 6 sites corretos, todos emlib/.Sites errados — decidem sem comparar o mês com
faturaActiveFrom:app/(app)/dashboard/page.tsx:54—const isFaturaMode = !isCycleView && creditMode.creditMode === 'fatura'. É a definição-raiz; os três seguintes derivam dela.app/(app)/dashboard/page.tsx:107-110—fixedForPendency, guardado porfaturaCtx && creditIdSet.size > 0(efaturaCtxsó existe quandoisFaturaMode). AlimentapendingFixed/unpaidFixedCount.app/(app)/dashboard/page.tsx:204—creditAccountIds={isFaturaMode ? faturaCtx?.creditAccountIds : undefined}no<FixedExpenseList>.app/(app)/dashboard/page.tsx:222— o mesmo ternário no<TransactionList>.app/(app)/configuracao-mes/page.tsx:147-153—<FixedExpenseList>renderizado semcreditAccountIdsnenhum, em página que nem chamagetUserCreditMode. Predicado ausente, não errado.Sites corretos — todos comparam o mês:
lib/queries/dashboard.ts:24-28(getCategoryGroupProgress) —referenceMonth >= faturaCtx.faturaActiveFromlib/queries/dashboard.ts:227-231(getDashboardData) — idemlib/queries/panorama.ts:81e:105(getAnnualOverview) — a comparação vai por linha, no SQL (lt(transactions.referenceMonth, faturaActiveFrom))lib/queries/panorama.ts:251-252(getAnnualExpensesByGroup) —t.referenceMonth >= faturaCtx!.faturaActiveFromlib/queries/fatura.ts:347—closedIsPreActivation = faturaActiveFrom !== null && closedCycleMonth < faturaActiveFromlib/actions/fatura.ts:127—createFaturaPaymentrecusa comcycle_before_activationPlacar no recorte que importa (quem decide o regime de um mês): 6 × 5, e os 5 são todos consumidores dos 6.
Evidência
getDashboardDatapublica a resposta certa e diz, em comentário, que a publica exatamente para o consumidor não reexpressar o filtro (lib/queries/dashboard.ts:268-272):A page recebe esse booleano, usa em um lugar (
:184, oCategoryGroupProgress) e, três linhas depois, reexpressa o filtro por conta própria — sem a metade que compara o mês.O efeito no componente não é cosmético.
components/dashboard/FixedExpenseList.tsx:96-110troca o botão por um disco inerte quandoisViaFatura:e suprime a
DueBadge(:158-166), que é quem emite "Vencido" / "Vence hoje".Cenário completo, mês pré-ativação.
components/contas/CreditModeSection.tsx:78-81fixamin={currentYearMonth()}no seletor e escreve o hint "Meses anteriores mantêm o comportamento atual." — ou seja,faturaActiveFromé sempre >= o mês da ativação, e todo o histórico do usuário é pré-ativação. Ativou em jun/2026, navega para mar/2026 pela seta doMonthSelector(normalizeYearMonthParam,lib/utils/date.ts:64-68, aceita qualquer ano > 2000 — não há piso emfaturaActiveFrom):getDashboardData('2026-03-01', ctx)→isFaturaMonthfalse → o gasto fixo do cartão entra emtotalExpensesde março. Correto, e é o que o roadmap promete.isFaturaModetrue → o mesmo gasto renderiza com selo "via fatura", sem checkbox de pago e sem badge de vencimento.O mesmo card, no mesmo render, afirma que a despesa é de março (no total) e que ela é de uma fatura que não existe (na linha). E o checkbox some de forma permanente para todo mês anterior à ativação — o usuário não consegue mais marcar como pago um fixo que ele de fato pagou.
pendingFixed(site 2) cai junto: o banner de pendências deixa de contar gastos fixos de crédito em meses onde o próprio roadmap manda contá-los.Cenário do site 5, mês sob fatura de verdade. Em
/configuracao-mes, com o regime ativo e o mês >=faturaActiveFrom, o gasto fixo do cartão aparece com o checkbox clicável e com badge "Vencido" em vermelho a partir do dia seguinte aodueDay— todo mês, para sempre, porque ninguém nunca o marca (o dashboard não deixa). A mesma linha, no dashboard, diz "via fatura" e não vence. Os dois sites de render deFixedExpenseListno repo são exatamente esses dois, e eles discordam.docs/regime-fatura/05-roadmap.md, Fase 5, tem o passo marcado como concluído:e o critério de aceite:
A page implementou o passo dropando o qualificador "em meses de fatura". E o "Inventário de Arquivos → Alterados" do mesmo roadmap não lista
app/(app)/configuracao-mes/page.tsx— a página nunca entrou no escopo do regime.Verificações feitas (tentativa de falsificar o achado)
grep -rn "faturaActiveFrom"emlib/,app/,components/: todo site delib/que decide o regime de um mês compara. Os 5 deapp/não. Não é discussão de arquitetura — é a minoria fora da convenção da maioria.panorama.ts:53-56e:186-189definemisFaturaModesem o mês. Fui ler o uso: a comparação está aplicada por linha, mais abaixo (:81,:105no SQL;:251-252em JS). Panorama acerta; o candidato "panorama também erra" morreu aqui.normalizeYearMonthParamsó rejeita ano <= 2000; não há piso emfaturaActiveFrom, e oMonthSelectornavega livremente. Emin={currentYearMonth()}no seletor de ativação garante que o conjunto pré-ativação nunca é vazio.git log -S: o clone é shallow (git rev-parse --is-shallow-repository→true, 237 commits,git blamedevolve^98c85f4de 2026-07-31 como boundary). Sem histórico, a falsificação foi pelo artefato que sobrevive: o roadmap do domínio registra a decisão contrária em dois pontos (passo da Fase 5 e critério de aceite), e o hint da própria UI de ativação promete o comportamento que a page quebra. Não é escolha registrada em lugar nenhum — é o qualificador que se perdeu ao descer delib/paraapp/.toggleFixedExpensePaidtem efeito financeiro? Não —greppor leituras de.paiddevolve só render (FixedExpenseList,CategoryGroupProgress:80) e a contagem de pendências. O impacto do site 5 é de UI e de alarme falso, não de saldo. Registrado para o implementador não procurar um bug de número que não existe.TransactionListsofre o mesmo? Sim, mas só cosmeticamente:TransactionList.tsx:168-172usaisViaFaturaapenas para o selo. Listado como site 4 para a correção fechar por inteiro, não como impacto próprio.Impacto
Usuário em regime de fatura, ao revisar qualquer mês anterior à ativação (que é todo o histórico dele, pela regra do
min): o dashboard soma a compra de cartão no total do mês e simultaneamente marca a linha como "via fatura"; o checkbox de "pago" do gasto fixo desaparece e não há como marcá-lo; o banner de pendências deixa de avisar sobre esses fixos. Contradiz o que a tela de ativação prometeu por escrito.Usuário em regime de fatura, em mês corrente:
/configuracao-mesacusa "Vencido" em vermelho, todo mês, nos gastos fixos do cartão que o dashboard trata como cobertos pela fatura — e oferece ali o toggle que o dashboard remove de propósito.Usuário em
accrual: zero diferença.faturaCtxéundefinedeisFaturaModeéfalsenos dois casos.Cobertura
O que já existe, e o que garante.
__tests__/integration/queries-fatura.test.ts:338—'maio: totalExpenses inclui crédito pois referenceMonth < faturaActiveFrom'— é exatamente o cenário do achado, e passa. Ele protege a query, e vai continuar verde depois da correção: não toca a page. É a peça que torna o achado indefensável como "comportamento pretendido" — o repo já assere, em teste de integração, o oposto do que a page renderiza no mesmo mês.queries-dashboard.test.ts:342e:414asseramcreditFilteredFromBudget(true em fatura, false em visão de ciclo); nenhum dos dois cobre o mês pré-ativação, e nenhum olha paraapp/.O que falta. Nada assere o predicado da page. Sem
@testing-library/reactno projeto, o teste de render não é a via (instalar a infra seria maior que a correção) — vale o precedente de gate por asserção sobre fonte já versionado em__tests__/unit/paginas-publicas.test.tse__tests__/unit/service-worker-registro.test.ts.Proposta em duas partes, ambas vermelhas hoje:
'2025-06-01'→true) não discrimina nada sozinho: a implementação errada também passa nele. Os dois juntos, sim.app/(app)/dashboard/page.tsxeapp/(app)/configuracao-mes/page.tsxnão podem contercreditMode === 'fatura'solto, e todo call site de<FixedExpenseList>/<TransactionList>tem de passarcreditAccountIds. Sem essa segunda metade, alguém adiciona o helper, deixa a page como está, e a suíte fica verde sobre o furo.Proposta
Parar de derivar o predicado na page. Duas correções distintas porque as duas pages têm dados distintos:
Dashboard (sites 1-4). Já tem a resposta pronta: trocar
isFaturaModepordata.creditFilteredFromBudgetnas linhas 107, 204 e 222. Ele é literalmenteisFaturaMonth && creditIdSet.size > 0(lib/queries/dashboard.ts:234), já éfalsena visão de ciclo (:403), e o comentário que o acompanha diz que existe para esse uso.isFaturaModecontinua válido onde ele de fato significa "o modo do usuário, não o do mês": montar ofaturaCtx(:56) e decidir se chamagetOpenFaturas/getPaymentAccounts(:74-75). Não mexer nesses dois./configuracao-mes(site 5). Não chamagetDashboardData, e chamá-la só para extrair um booleano puxaria quatro queries a mais. Precisa degetUserCreditMode+getCreditAccountsnoPromise.allque já existe (:46-51) e do predicado compartilhado.Helper, e por que esse. Exportar
isFaturaMonth(referenceMonth: string, faturaCtx?: FaturaContext): booleandelib/queries/fatura.ts, e reusá-lo nos dois sites delib/queries/dashboard.tsque hoje o repetem inline.lib/utils/date.ts: é para lá que a mão vai primeiro, porque a expressão parece comparação de mês — mas o predicado também depende decreditModee da existência de contas de crédito, que são estado de domínio, não aritmética de calendário.date.tsnão conheceFaturaContexte não deveria passar a conhecer.getFaturaState: responde sobre um ciclo de uma conta, faz I/O, e a page precisa de um booleano puro sobre um mês.lib/queries/fatura.tsé ondeFaturaContextjá vive, e ondeclosedIsPreActivation(:347) já expressa a metade negativa do mesmo predicado.Cuidado ao implementar: a correção deixa a suíte verde — nenhum teste existente muda de cor. O sinal de que funcionou são os dois casos novos da seção Cobertura.
Custo estimado
M —
app/(app)/dashboard/page.tsx,app/(app)/configuracao-mes/page.tsx,lib/queries/fatura.ts(helper) elib/queries/dashboard.ts(passa a reusar o helper), mais o arquivo de teste.