Skip to content

[arquitetura] "este mês está sob regime de fatura" é decidido na page, e as duas pages decidem diferente — o dashboard esquece a comparação com faturaActiveFrom e /configuracao-mes não compara nada #146

Description

@Guiroos

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:

  1. app/(app)/dashboard/page.tsx:54const isFaturaMode = !isCycleView && creditMode.creditMode === 'fatura'. É a definição-raiz; os três seguintes derivam dela.
  2. app/(app)/dashboard/page.tsx:107-110fixedForPendency, guardado por faturaCtx && creditIdSet.size > 0 (e faturaCtx só existe quando isFaturaMode). Alimenta pendingFixed/unpaidFixedCount.
  3. app/(app)/dashboard/page.tsx:204creditAccountIds={isFaturaMode ? faturaCtx?.creditAccountIds : undefined} no <FixedExpenseList>.
  4. app/(app)/dashboard/page.tsx:222 — o mesmo ternário no <TransactionList>.
  5. 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:347closedIsPreActivation = faturaActiveFrom !== null && closedCycleMonth < faturaActiveFrom
  • lib/actions/fatura.ts:127createFaturaPayment 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:

  • Ajustar unpaidFixedCount para não alertar gastos fixos de crédito em meses de fatura

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-repositorytrue, 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:

  1. Unit test do helper novo, com a entrada que 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions