feat(platform): Billing > Fechamentos e Relatórios

GET /billing/periods e /billing/statements só serviam o próprio tenant do
JWT — sem uso pra um platform admin escolhendo um tenant arbitrário.
Adicionado GET .../by-tenant/:tenantId nos dois (mesmo padrão já usado em
Subscriptions), e GET /billing/statements/:id ganhou um ?tenantId=
opcional só aceito de quem tem role de plataforma.

Frontend: /platform/billing/fechamentos (fecha/reabre período por
tenant, seletor via querystring pra não duplicar rota) e /relatorios
(statements por tenant, detalhe com itens por categoria).

Bug real achado testando o fluxo: fechar um período de 01/08 a 31/08
mostrava "31 de jul." a "30 de ago." — meia-noite UTC de uma data-only
vira o dia anterior no timezone local do servidor. Corrigido com
formatDateUTC novo, usado só em fronteiras de calendário (não em
timestamps de verdade, que continuam com formatDate local).

Testado ponta a ponta contra a API real: período fechado, statement
gerado (R$ 0,00 honesto — Acme sem assinatura/price book ainda), detalhe
correto. Smoke test nas 19 telas do tenant + 8 telas platform, todas 200.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-29 20:31:12 -03:00
parent a23e68b011
commit c95c6805fb
18 changed files with 670 additions and 6 deletions

47
TODO.md
View File

@@ -1287,6 +1287,53 @@ Saúde (agente.md secao 148, 150-151, 168, 187)
platform-wide (ver GLOBAL, agregar uso/custo entre tenants) não foi
construída ainda
## PHASE 35 — Platform: Billing > Fechamentos e Relatórios (agente.md
secao 134-139, 168)
- [x] **Lacuna de backend fechada primeiro**: `GET /billing/periods` e
`GET /billing/statements` só serviam o próprio tenant do JWT — sem
uso pra um platform admin escolhendo um tenant arbitrário (a mesma
exceção já resolvida em Subscriptions na PHASE 22, só não tinha
sido replicada aqui ainda). Adicionado `GET /billing/periods/by-
tenant/:tenantId` e `GET /billing/statements/by-tenant/:tenantId`
(mesmo padrão), e `GET /billing/statements/:id` ganhou um
`?tenantId=` opcional só aceito de quem tem role de plataforma
(nunca confiado sem essa checagem, secao 31).
- [x] Frontend: `/platform/billing/fechamentos` (seletor de tenant via
`?tenantId=` na própria URL — sem isso, 4 itens de menu
apontariam pro mesmo lugar; um seletor dentro da página resolve sem
duplicar rota) — lista períodos, fecha um novo (intervalo de
datas), reabre um fechado com motivo obrigatório.
`/platform/billing/relatorios` — lista statements por tenant,
detalhe com itens por categoria + subtotal/ajustes/total. Nunca
chamado de "nota fiscal" na UI (PRODUCT.md).
- [x] **Bug real, achado testando o fluxo completo**: fechar um período
de 01/08 a 31/08 mostrava "31 de jul." a "30 de ago." na tela —
meia-noite UTC de uma data-only vira o dia anterior quando
formatada no timezone local do servidor (America/Sao_Paulo,
UTC-3). `formatDate` (local) está certo pra timestamps de verdade
(criado em, gerado em), mas errado pra fronteiras de calendário.
Corrigido com `formatDateUTC` novo em `lib/format.ts`, usado só
onde o valor é uma fronteira de período, não um instante.
- Achado incidental durante a investigação (não um bug de produto):
um 404 na tela de detalhe do statement era eu mesmo esquecendo de
reiniciar `apps/api` depois de editar o `get()` do controller —
confirmado isolando com um cliente Prisma direto (achou a linha
sem problema) antes de suspeitar da camada HTTP.
- [x] Testado ponta a ponta contra a API real: fechado um período de
agosto/2026 pro tenant Acme (sem assinatura/price book atribuído
ainda, então R$ 0,00 — honesto, não um erro), aparece em
Fechamentos com as datas certas, gera um statement visível em
Relatórios, detalhe mostra 0 itens/subtotal/total corretos. Smoke
test de regressão nas 19 telas do tenant + 8 telas platform, todas
200.
- [ ] "Billing > Consumo" continua "em breve" — distinção de escopo com
"Relatórios" nunca ficou 100% clara na especificação (secao 138 vs
135); vai depender de decidir se é uma view de uso corrente
(período aberto) ou se sobrepõe com o dashboard de plataforma
- [ ] Sem exclusão de statement nem edição manual de item — statements
são gerados, nunca editados à mão (consistente com "fechamento
imutável", secao 137)
---
## Riscos conhecidos