Skip to content

fix: ancorar término de parcelas em nextChargeMonth, não no mês atual (closes #114) - #132

Draft
Guiroos wants to merge 4 commits into
mainfrom
claude/quirky-johnson-a64mgd
Draft

fix: ancorar término de parcelas em nextChargeMonth, não no mês atual (closes #114)#132
Guiroos wants to merge 4 commits into
mainfrom
claude/quirky-johnson-a64mgd

Conversation

@Guiroos

@Guiroos Guiroos commented Aug 29, 2026

Copy link
Copy Markdown
Owner

O que mudou

app/(app)/parcelas/page.tsx calculava o "termina em" de cada grupo de parcelas com aritmética de mês feita à mão (calcEndLabel), sempre ancorada no mês atual. Para um parcelamento cuja 1ª parcela cai no mês seguinte (compra feita depois do dia de fechamento do cartão — o caminho comum, não um caso de borda), isso projetava as remainingInstallments parcelas a partir de hoje em vez de a partir do mês em que elas de fato começam, adiantando em 1 mês o "termina em" exibido nos cards de InstallmentGroupCard e nos dois KPIs de topo (totalRestante/lastEnd).

  1. Removida calcEndLabel (aritmética manual dentro da page).
  2. Adicionado addMonthsToYearMonth(yearMonth, n) em lib/utils/date.ts — generalização de nextMonth, usando as mesmas peças (addMonths + format).
  3. page.tsx agora ancora em g.nextChargeMonth (já calculado pela query getActiveInstallmentGroups, antes ignorado pela page), com fallback para o mês atual quando não há parcela futura pendente (nextChargeMonth === null):
    endLabel: formatMonthShort(
      addMonthsToYearMonth(g.nextChargeMonth ?? currentYM, g.remainingInstallments - 1)
    )

Por que dessa forma

Segui a proposta da issue à risca — inclusive a escolha do helper. futureNMonths(n) (o helper "óbvio") ancora em startOfMonth(new Date()), exatamente a âncora que causa o bug. monthOptions(...).at(-1) daria o valor certo mas materializa a faixa inteira e seu contrato é "opções de seletor de mês", não aritmética pontual. addMonthsToYearMonth é a generalização mínima de nextMonth (mesmas peças, parâmetro n).

Não toquei getInstallmentTimeline (lib/queries/parcelas.ts), que usa futureNMonths(12) — ali a âncora em hoje é o comportamento correto (linha do tempo dos próximos 12 meses de calendário), e não em paidInstallments/remainingInstallments, que já estão corretos e alimentam também a exportação completa.

Como testei

  • npm run lint && npm run format:check && npm run typecheck && npm test && npm run build — todos verdes.
  • Novos testes em __tests__/unit/date.test.ts para addMonthsToYearMonth, incluindo o caso do próprio achado: addMonthsToYearMonth('2026-09', 11) deve ser '2027-08', e a correção errada mais provável (manter âncora no mês atual) — addMonthsToYearMonth('2026-08', 11) = '2027-07' — fica registrada como o valor que a correção não deve produzir.
  • ds-reviewer rodado sobre app/(app)/parcelas/page.tsx (hook PostToolUse:Edit exige a cada edição do arquivo): confirmou que a mudança é puramente lógica, sem regressão de UI. O agente reportou 2 violações de DS pré-existentes no arquivo (uso de uppercase sobre tokens text-label/text-caption, e tabular-nums ausente em duas contagens) — fora do escopo desta issue, não tocadas.

Risco e o que NÃO foi coberto

Arquivos tocados

  • app/(app)/parcelas/page.tsx
  • lib/utils/date.ts
  • __tests__/unit/date.test.ts

Generated by Claude Code

…closes #114)

/parcelas calculava "termina em" com aritmetica manual ancorada no mes
corrente. Para parcelamentos cuja 1a parcela cai no mes seguinte (compra
apos o fechamento do cartao), isso projetava as parcelas restantes a
partir de hoje em vez de a partir de quando elas de fato comecam,
adiantando o "termina em" exibido nos cards e nos dois KPIs do topo.

Extrai addMonthsToYearMonth para lib/utils/date.ts e ancora o calculo em
g.nextChargeMonth (com fallback para o mes atual quando nao ha parcela
futura pendente).

@Guiroos Guiroos left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisão do head af914a7. A âncora em nextChargeMonth está certa, o helper novo é a generalização mínima de nextMonth como a issue pediu, e a decisão de não tocar getInstallmentTimeline / remainingInstallments está bem justificada no corpo. Dois achados verificados, ambos bloqueantes; nenhum nit.

1. Os testes novos passam igual com a correção errada (__tests__/unit/date.test.ts:170-175). Eles exercitam só o helper puro; nenhum teste alcança page.tsx. Revertendo a âncora para currentYM, a suíte segue verde. A issue nomeou o caso exato e disse que extrair a derivação da page era pré-requisito para cobrir — os dois foram substituídos sem declaração.

2. lastEnd ordena rótulos formatados, não meses (app/(app)/parcelas/page.tsx:40). formatMonthShort devolve "set 26" / "jan 27", e .sort() alfabético é month-major — os dois KPIs de topo seguem mostrando o mês errado. Pré-existente, mas dentro do closes #114, que lista lastEnd na seção Onde.

Detalhes e correção concreta em cada comentário inline.


Generated by Claude Code

Comment thread __tests__/unit/date.test.ts Outdated
Comment thread app/(app)/parcelas/page.tsx Outdated
Endereca dois achados bloqueantes da revisao da PR #132 (issue #114):

1. Os testes anteriores exercitavam so addMonthsToYearMonth (aritmetica
   pura), nunca a logica de ancora que era o proprio bug -- revertendo
   page.tsx para ancorar em currentYM a suite continuava verde. Extrai
   installmentEndYearMonth para lib/utils/date.ts, unica fonte usada por
   page.tsx, e testa os dois casos que a issue #114 pedia (ancora em
   nextChargeMonth e fallback nextChargeMonth=null) sob fake timers.

2. lastEnd ordenava os rotulos ja formatados por formatMonthShort
   ('set 26', 'jan 27'), que e ordem alfabetica (month-major), nao
   cronologica -- os dois KPIs de topo continuavam errados mesmo apos a
   correcao do endLabel por card. Passa a ordenar por endYM (YYYY-MM,
   ano-major) e so formata o maximo no final.

@Guiroos Guiroos left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisão do head de435b3 (a anterior foi no af914a7). Os dois achados bloqueantes daquela rodada estão resolvidos de verdade: installmentEndYearMonth encapsula a escolha da âncora e é a única fonte usada pela page, com os dois casos que a #114 nomeou (âncora em nextChargeMonth e fallback null) sob fake timers; e o lastEnd passou a ordenar por endYM (YYYY-MM, ano-major) e só formatar o máximo no fim — o .sort() alfabético month-major que eu tinha apontado sumiu.

Também confirmei o formato da âncora, que era o risco silencioso da mudança: nextChargeMonth vem de referenceMonth.slice(0, 7) (lib/queries/parcelas.ts:76), ou seja YYYY-MM, que é o que parseISO(\${yearMonth}-01`)espera — não umYYYY-MM-01que estouraria emRangeError`.

Rodei os gates nesta sessão, sobre este head: npm ci, npm run lint, npm run typecheck e npm test verdes (34 arquivos, 532 testes).

Um achado, bloqueante — e é uma consequência do próprio diff, não uma lacuna da issue. A ordenação "Termina mais cedo" (ParcelasToolbar.tsx:45) ordena por remainingInstallments, o que era equivalente ao endLabel exibido enquanto todos os grupos partiam da mesma âncora, e deixou de ser agora que cada um parte do seu nextChargeMonth. Verifiquei rodando: com dois grupos plausíveis, a lista aparece ordenada ao contrário do que os próprios cards dizem no rótulo "termina". Correção de uma linha, com o dado que este PR já criou (endYM), no comentário inline.


Generated by Claude Code

Comment thread app/(app)/parcelas/page.tsx
…ents

Achado bloqueante da revisao da PR #132 (issue #114): a correcao da ancora
por grupo (nextChargeMonth) quebrou o pressuposto que o sort 'soonest-end'
de ParcelasToolbar.tsx dependia -- antes todos os grupos compartilhavam a
mesma ancora (mes atual), entao remainingInstallments era uma proxy valida
para "termina em". Com ancora por grupo, as duas ordens divergem, e a
tela passou a mostrar cards ordenados por "Termina mais cedo" que
contradizem o proprio rotulo "termina" de cada card.

Ordena por endYM (YYYY-MM, ano-major) via localeCompare, mesmo dado que
page.tsx ja produz para o lastEnd. applySort exportada e testada em
__tests__/unit/parcelas-toolbar.test.ts com o caso que so a correcao
certa passa: dois grupos cujo remainingInstallments e endYM apontam para
ordens opostas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc5r53ktBJD1ZZL2oDEyPG

@Guiroos Guiroos left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisão do head a26955c (as anteriores foram no af914a7 e no de435b3). O achado bloqueante da rodada passada está resolvido: soonest-end passou a ordenar por endYM (ParcelasToolbar.tsx:46), que é o mesmo dado que alimenta o rótulo "termina" do card (InstallmentGroupCard.tsx:130-134) — lista e card voltam a concordar.

Conferi por conta própria que o endYM chega mesmo ao comparador, em vez de aceitar pelo diff: a page o propaga em groupsWithEnd (app/(app)/parcelas/page.tsx:33-36) e <ParcelasToolbar groups={groupsWithEnd}> (:230) é o único consumidor do componente no repo.

Rodei os gates nesta sessão, sobre este head: npm ci, npm test (35 arquivos, 536 testes) e npx tsc --noEmit verdes.

O it() principal do arquivo novo cobre de verdade — mutei o comparador de volta para remainingInstallments e ele fica vermelho sozinho, sem arrastar os outros.

Um achado, nit — nenhum bloqueante. O segundo it() do mesmo describe (:41-48) passa igual com o bug e com o comparador virando no-op; uma linha resolve. As duas mutações que rodei e a correção verificada estão no comentário inline.

Uma nota que não é achado, registrada para não voltar como pergunta: endYM?: string no tipo Group (ParcelasToolbar.tsx:33) é opcional, então o comparador degrada em silêncio para '' se alguém montar a toolbar sem o campo. Não abro porque o caminho realista já é barrado pelo compilador — tirei endYM do map da page e tsc reprova em page.tsx:40, onde lastEndYM lê o campo.


Generated by Claude Code

Comment thread __tests__/unit/parcelas-toolbar.test.ts Outdated
… bug

Nit da revisao da PR #132 (issue #114): a entrada [a, b] do teste de
endYM ausente ja estava na ordem esperada pela asserção, e Array.sort e
estavel -- entao qualquer comparador que empate (ou vire no-op) devolve
['A', 'B'] sem exercitar nada. Invertida para [b, a], igual ao primeiro
it() do arquivo, que ja usava essa entrada e de fato falha se
soonest-end voltar a ordenar por remainingInstallments.

Verificado revertendo o comparador do PR e rodando so este arquivo: os
dois it()s de soonest-end falham de forma independente agora.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc5r53ktBJD1ZZL2oDEyPG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants