You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[dados] Recorte do /historico não alcança entradas e aportes — filtrar 15 a 31 de agosto devolve o salário de 01/08, sob um cabeçalho de data fora do intervalo #134
Dos 5 tipos que collectHistoricoItems mistura, 3 garantem que a data exibida cai dentro de [de, ate] e 2 não — a minoria é o defeito (categoria 5 do .claude/audit.md), contada no recorte da própria função e não no repo inteiro:
Tipo
Filtro de data
Dentro de [de,ate]?
saida_avulsa / saida_parcelada
between(transactions.date, de, ate) — SQL (:81)
✅
resgate
between(investmentWithdrawals.date, de, ate) — SQL (:108)
✅
saida_fixa
mês no IN + filtro JS pela data de exibição (:253)
✅
entrada
só inArray(incomes.referenceMonth, refMonths) (:98)
❌
investimento
só inArray(investments.referenceMonth, refMonths) (:103)
❌
referenceMonthsInRange (:51-61) expande o intervalo para meses inteiros — e está certo em fazer isso, porque um gasto fixo com referenceMonth = M pode exibir em M+1 via dueDay (é exatamente o que __tests__/integration/export-queries.test.ts:174-202 prova). Só que incomes.referenceMonth e investments.referenceMonth são sempre YYYY-MM-01 (referenceMonthSchema, lib/validations/utils.ts:50-52: /^\d{4}-\d{2}-01$/), e o item é exibido nessa data. Então todo mês que o IN traz por sobreposição entrega entrada/aporte datados no dia 1º, mesmo quando de é posterior.
Rodei a aritmética do recorte isoladamente (as duas funções copiadas de historico.ts, sem dependência):
de=2026-08-15 ate=2026-08-31 refMonths=["2026-08-01"]
entrada/aporte datados FORA de [de,ate] e mesmo assim devolvidos: ["2026-08-01"]
(gasto fixo dueDay=1 no mesmo mês -> exibe 2026-08-01 -> mantido? false)
de=2026-08-10 ate=2026-08-20 refMonths=["2026-08-01"]
entrada/aporte datados FORA de [de,ate] e mesmo assim devolvidos: ["2026-08-01"]
(gasto fixo dueDay=1 no mesmo mês -> exibe 2026-08-01 -> mantido? false)
de=2026-06-02 ate=2026-08-31 refMonths=["2026-06-01","2026-07-01","2026-08-01"]
entrada/aporte datados FORA de [de,ate] e mesmo assim devolvidos: ["2026-06-01"]
(gasto fixo dueDay=1 no mesmo mês -> exibe 2026-06-01 -> mantido? false)
A terceira linha é o intervalo default (últimos 90 dias, historico-params.ts:30-34), então o vazamento não depende de o usuário mexer em filtro nenhum.
E a incoerência não é teórica: na segunda linha, um gasto fixo com dueDay = 1 e uma entrada, ambos exibidos em 2026-08-01, dentro da mesma chamada e do mesmo intervalo — o gasto fixo é descartado pela linha 253 e a entrada passa. A mesma função já decidiu que data de exibição fora da janela desqualifica o item; ela só não aplicou a decisão a dois dos cinco tipos.
Verificações feitas (tentativa de falsificar o achado)
"Entrada é mensal, então mês-sobrepõe-intervalo seria o design correto." Não se sustenta: o item tem data concreta, ela é renderizada como cabeçalho de grupo, e o próprio arquivo já rejeitou essa leitura para gasto fixo na linha 253. Um mesmo 2026-08-01 sair e entrar no mesmo resultado não é granularidade de mês, é inconsistência.
referenceMonth pode ter dia diferente de 01? Não — referenceMonthSchema é regex ancorada em -01. Todo item de entrada/aporte é datado no dia 1º, logo qualquer de posterior ao dia 1º do mês vaza.
Existe omissão além do vazamento? Não. refMonths sempre contém todos os meses que sobrepõem o intervalo, e a data do item é o 1º daquele mês — o erro é só inclusão indevida, nunca item faltando. Isso mantém o impacto simples e a correção segura.
A superfície tem consumidor? (exigência 7) Duas, ambas vivas: app/(app)/historico/page.tsx:26 → HistoricoClient, que agrupa por item.date e imprime um cabeçalho por data (HistoricoClient.tsx:48-51 monta os grupos, :212-215 renderiza date={formatGroupDate(date)}); e app/api/export/extrato/route.ts:22, que exporta os mesmos itens em .xlsx/.csv.
git log -S "Filtro JS de precisão para fixedExpenses" → só 98c85f4, o import inicial do repo. Não há commit defendendo a exclusão de entradas/aportes do filtro; a linha nasceu resolvendo o caso do dueDay e nunca foi generalizada.
Impacto
Qualquer usuário que estreite o recorte do /historico para dentro de um mês — "primeira quinzena", "de 10 a 20", conferir uma semana específica. A entrada de salário e os aportes do mês inteiro aparecem no feed, sob um cabeçalho de data anterior ao início que ele acabou de escolher. Como entrada costuma ser o maior valor do mês, o extrato de um recorte de 5 dias pode vir dominado por um lançamento que não pertence a ele.
O mesmo resultado vai para o arquivo exportado, cujo nome afirma o recorte: mare-extrato-2026-08-15-a-2026-08-31.xlsx (app/api/export/extrato/route.ts:25-27) contendo uma linha datada 2026-08-01.
O intervalo default (90 dias) também vaza, mas só o mês da ponta — um item. O caso caro é o recorte curto e deliberado.
Cobertura
Existe teste sobre a função, e é preciso dizer o que ele garante (exigência 3), porque ele aponta para a correção errada:
__tests__/unit/historico-merge.test.ts:95-108 testa referenceMonthsInRangeisolada e assere a expansão para meses inteiros ('2024-12-15','2025-01-10' → ['2024-12-01','2025-01-01']). Esse comportamento está correto e deve continuar verde — estreitar referenceMonthsInRange é a "correção óbvia" e é a errada.
__tests__/integration/export-queries.test.ts:174-202 é a prova de que estreitar quebra: um "Aluguel" com referenceMonth 2024-06-01 e dueDay 20 só entra no recorte porque o mês 2024-06-01 está no IN. Se o IN passar a exigir o mês inteiro dentro de [de,ate], esse teste fica vermelho.
Ou seja: a suíte hoje protege o pré-filtro do SQL e não cobre nada do recorte final. Nenhum teste chama collectHistoricoItems com de posterior ao dia 1º.
Caso proposto (integração, junto de export-queries.test.ts ou em arquivo próprio de collectHistoricoItems), com a entrada que só a correção certa rejeita:
Usuário com uma income em referenceMonth = 2025-08-01e um fixedExpense em referenceMonth = 2025-08-01, dueDay = 25.
collectHistoricoItems({ de: '2025-08-15', ate: '2025-08-31', tipos: [...ALL_TIPOS], ... })
Esperar: a entrada não aparece (falha hoje) e o gasto fixo aparece (exibe em 2025-08-25).
O gasto fixo no mesmo caso é o que dá o poder discriminante: ele obriga o mês 2025-08-01 a continuar no IN. Um teste só com a entrada passaria também na correção errada (estreitar referenceMonthsInRange), que derruba o gasto fixo junto e quebra export-queries.test.ts — a suíte acusaria em outro arquivo, longe da mudança, e o sinal se perderia. Com os dois no mesmo it, a única implementação que passa é a certa.
Proposta
Promover o filtro da linha 253 de "só fxItems" para "todo o feed", aplicando-o depois do merge em lib/queries/historico.ts:
e remover fxItemsFiltered (:253), que passa a ser redundante.
Por que esse predicado nesse ponto, e não um gte/lte sobre referenceMonth no SQL de incomes/investments (exigência 6): o where do SQL continua tendo de trazer meses inteiros por causa do gasto fixo, então um bound adicional só para duas das cinco tabelas passaria a expressar "o que está no recorte" em dois lugares com regras diferentes — que é o defeito que esta issue relata, reintroduzido um nível abaixo. O filtro único pós-merge deixa uma definição só, opera sobre HistoricoFeedItem.date (o campo que a UI de fato imprime) e é no-op para txItems/withdrawItems, já filtrados no SQL.
Efeito colateral desejável: getHistoricoFeed pagina sobre essa lista (:277-287), então a contagem, o hasMore e o cursor passam a refletir o recorte pedido.
Custo estimado
P — 1 arquivo (lib/queries/historico.ts), duas linhas.
Onde
lib/queries/historico.ts:252-253— o filtro de precisão do recorte existe, mas cobre só um dos três tipos que precisam dele:Os itens que ficam de fora dele:
lib/queries/historico.ts:193-208—incomeItems,date: i.referenceMonthlib/queries/historico.ts:210-230—investItems,date: inv.referenceMonthEvidência
Dos 5 tipos que
collectHistoricoItemsmistura, 3 garantem que a data exibida cai dentro de[de, ate]e 2 não — a minoria é o defeito (categoria 5 do.claude/audit.md), contada no recorte da própria função e não no repo inteiro:[de,ate]?saida_avulsa/saida_parceladabetween(transactions.date, de, ate)— SQL (:81)resgatebetween(investmentWithdrawals.date, de, ate)— SQL (:108)saida_fixaIN+ filtro JS pela data de exibição (:253)entradainArray(incomes.referenceMonth, refMonths)(:98)investimentoinArray(investments.referenceMonth, refMonths)(:103)referenceMonthsInRange(:51-61) expande o intervalo para meses inteiros — e está certo em fazer isso, porque um gasto fixo comreferenceMonth = Mpode exibir emM+1viadueDay(é exatamente o que__tests__/integration/export-queries.test.ts:174-202prova). Só queincomes.referenceMontheinvestments.referenceMonthsão sempreYYYY-MM-01(referenceMonthSchema,lib/validations/utils.ts:50-52:/^\d{4}-\d{2}-01$/), e o item é exibido nessa data. Então todo mês que oINtraz por sobreposição entrega entrada/aporte datados no dia 1º, mesmo quandodeé posterior.Rodei a aritmética do recorte isoladamente (as duas funções copiadas de
historico.ts, sem dependência):A terceira linha é o intervalo default (últimos 90 dias,
historico-params.ts:30-34), então o vazamento não depende de o usuário mexer em filtro nenhum.E a incoerência não é teórica: na segunda linha, um gasto fixo com
dueDay = 1e uma entrada, ambos exibidos em2026-08-01, dentro da mesma chamada e do mesmo intervalo — o gasto fixo é descartado pela linha 253 e a entrada passa. A mesma função já decidiu que data de exibição fora da janela desqualifica o item; ela só não aplicou a decisão a dois dos cinco tipos.Verificações feitas (tentativa de falsificar o achado)
2026-08-01sair e entrar no mesmo resultado não é granularidade de mês, é inconsistência.referenceMonthpode ter dia diferente de 01? Não —referenceMonthSchemaé regex ancorada em-01. Todo item de entrada/aporte é datado no dia 1º, logo qualquerdeposterior ao dia 1º do mês vaza.refMonthssempre contém todos os meses que sobrepõem o intervalo, e a data do item é o 1º daquele mês — o erro é só inclusão indevida, nunca item faltando. Isso mantém o impacto simples e a correção segura.app/(app)/historico/page.tsx:26→HistoricoClient, que agrupa poritem.datee imprime um cabeçalho por data (HistoricoClient.tsx:48-51monta os grupos,:212-215renderizadate={formatGroupDate(date)}); eapp/api/export/extrato/route.ts:22, que exporta os mesmos itens em.xlsx/.csv./historicosão de outra coisa: [types] parseHistoricoParams tipade/atecomo datas mas devolve a query string crua, sem validação #32 (validação dede/ate), [types]?categorias=/?contas=do /historico chegam crus às colunas uuid — a mesma função validatipos,deeatetrês linhas acima #110 (?categorias=/?contas=sem validar UUID), [dados] Exportação completa descarta gasto fixo que vence depois do teto — o teto é MAX(referenceMonth), a exibição é referenceMonth + dueDay #97 (teto da exportação completa para gasto fixo).search_issuesporcollectHistoricoItems/referenceMonthsInRangenão devolve nada desta natureza.git log -S "Filtro JS de precisão para fixedExpenses"→ só98c85f4, o import inicial do repo. Não há commit defendendo a exclusão de entradas/aportes do filtro; a linha nasceu resolvendo o caso dodueDaye nunca foi generalizada.Impacto
Qualquer usuário que estreite o recorte do
/historicopara dentro de um mês — "primeira quinzena", "de 10 a 20", conferir uma semana específica. A entrada de salário e os aportes do mês inteiro aparecem no feed, sob um cabeçalho de data anterior ao início que ele acabou de escolher. Como entrada costuma ser o maior valor do mês, o extrato de um recorte de 5 dias pode vir dominado por um lançamento que não pertence a ele.O mesmo resultado vai para o arquivo exportado, cujo nome afirma o recorte:
mare-extrato-2026-08-15-a-2026-08-31.xlsx(app/api/export/extrato/route.ts:25-27) contendo uma linha datada2026-08-01.O intervalo default (90 dias) também vaza, mas só o mês da ponta — um item. O caso caro é o recorte curto e deliberado.
Cobertura
Existe teste sobre a função, e é preciso dizer o que ele garante (exigência 3), porque ele aponta para a correção errada:
__tests__/unit/historico-merge.test.ts:95-108testareferenceMonthsInRangeisolada e assere a expansão para meses inteiros ('2024-12-15','2025-01-10'→['2024-12-01','2025-01-01']). Esse comportamento está correto e deve continuar verde — estreitarreferenceMonthsInRangeé a "correção óbvia" e é a errada.__tests__/integration/export-queries.test.ts:174-202é a prova de que estreitar quebra: um "Aluguel" comreferenceMonth 2024-06-01edueDay 20só entra no recorte porque o mês2024-06-01está noIN. Se oINpassar a exigir o mês inteiro dentro de[de,ate], esse teste fica vermelho.Ou seja: a suíte hoje protege o pré-filtro do SQL e não cobre nada do recorte final. Nenhum teste chama
collectHistoricoItemscomdeposterior ao dia 1º.Caso proposto (integração, junto de
export-queries.test.tsou em arquivo próprio decollectHistoricoItems), com a entrada que só a correção certa rejeita:incomeemreferenceMonth = 2025-08-01e umfixedExpenseemreferenceMonth = 2025-08-01,dueDay = 25.collectHistoricoItems({ de: '2025-08-15', ate: '2025-08-31', tipos: [...ALL_TIPOS], ... })2025-08-25).O gasto fixo no mesmo caso é o que dá o poder discriminante: ele obriga o mês
2025-08-01a continuar noIN. Um teste só com a entrada passaria também na correção errada (estreitarreferenceMonthsInRange), que derruba o gasto fixo junto e quebraexport-queries.test.ts— a suíte acusaria em outro arquivo, longe da mudança, e o sinal se perderia. Com os dois no mesmoit, a única implementação que passa é a certa.Proposta
Promover o filtro da linha 253 de "só
fxItems" para "todo o feed", aplicando-o depois do merge emlib/queries/historico.ts:e remover
fxItemsFiltered(:253), que passa a ser redundante.Por que esse predicado nesse ponto, e não um
gte/ltesobrereferenceMonthno SQL de incomes/investments (exigência 6): owheredo SQL continua tendo de trazer meses inteiros por causa do gasto fixo, então um bound adicional só para duas das cinco tabelas passaria a expressar "o que está no recorte" em dois lugares com regras diferentes — que é o defeito que esta issue relata, reintroduzido um nível abaixo. O filtro único pós-merge deixa uma definição só, opera sobreHistoricoFeedItem.date(o campo que a UI de fato imprime) e é no-op paratxItems/withdrawItems, já filtrados no SQL.Efeito colateral desejável:
getHistoricoFeedpagina sobre essa lista (:277-287), então a contagem, ohasMoree o cursor passam a refletir o recorte pedido.Custo estimado
P — 1 arquivo (
lib/queries/historico.ts), duas linhas.