feat(billing): rating engine, fechamento de periodo, dashboard platform (fase 22)

Fecha a orquestracao de Billing (agente.md secao 120-139) sobre o schema/
RatingEngine puro ja existentes: escritores do ledger UsageEvent
(CALL_SECONDS no CDR, ACTIVE_DAY via sweep diario), closeBillingPeriod/
reopenBillingPeriod (fechamento imutavel com audit trail), e os
controllers de price books/rate decks/plan versions/subscriptions/
periods/statements. Corrige 2 bugs reais de RLS achados no teste ponta a
ponta (reopen sem tenant context, subscriptions sem withTenantContext) e
adiciona teste unitario do RatingEngine (17 casos).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
This commit is contained in:
2026-08-29 00:28:35 -03:00
parent d4e2513764
commit b27cfaab02
35 changed files with 4705 additions and 34 deletions

65
TODO.md
View File

@@ -589,8 +589,69 @@ docs/QUALITY_SCORECARDS.md
(mesma restrição de todo o módulo de IA); uma `QualityEvaluation`
completando de verdade (só via dead-letter, sem rede real)
## PHASE 22+ver `agente.md` seções 120 em diante (Usage Metering
completo, Billing, Frontend, Security, Tests)
## PHASE 22 — Usage Metering / Billing (agente.md secao 120-139) — ver
docs/BILLING.md
- [x] Migration `billing` (`PlanVersion`/`TenantSubscription`/`PriceBook`+
`PriceBookItem`/`RateDeck`+`RateDeckEntry`/`UsageEvent`/
`RatedUsageItem`/`BillingPeriod`/`BillingStatement`+
`BillingStatementItem`, RLS nas tenant-scoped) e `packages/billing`
(`RatingEngine` puro: longest-prefix match, rating por destino/
fallback plano, prorateio de dias ativos, tokens/transcrição de IA,
armazenamento) já existiam de uma sessão anterior interrompida —
só a orquestração (I/O) e os endpoints faltavam
- [x] `apps/api/src/billing/billing-engine.service.ts`:
`closeBillingPeriod`/`reopenBillingPeriod` — lê os 2 ledgers
imutáveis (`UsageEvent`+`AIUsageRecord`) ainda não tarifados
(`ratedUsageItems: { none: {} }`), grava 1 `RatedUsageItem` por
evento (nunca agrega antes de ratear), soma `PLAN_BASE` da
`TenantSubscription` ativa, agrega por categoria em
`BillingStatementItem`. Fechamento imutável (secao 137): `CLOSED`
de novo é 409, só reopen explícito (audit trail) permite recalcular
- [x] Escritores do ledger `UsageEvent`: `CALL_SECONDS` em
`apps/freeswitch-events/src/cdr.ts::finalizeCall` (mesma transação
do CDR); `EXTENSION_ACTIVE_DAY`/`AGENT_ACTIVE_DAY`/
`TRUNK_ACTIVE_DAY` em `apps/api/src/billing/active-day-sweep.ts`
(boot + hora em hora, idempotente por dia)
- [x] Controllers: `PriceBooksController`/`RateDecksController`/
`PlanVersionsController` (catálogo global, `pricing.manage` +
`isPlatformUser`), `SubscriptionsController`/
`BillingPeriodsController` (`billing.manage` + `isPlatformUser`,
`tenantId` explícito no body — ação de platform admin sobre um
tenant arbitrário), `BillingStatementsController` (`billing.view`,
sempre o próprio tenant do JWT)
- [x] **Bug real, achado no teste desta fase**: `reopenBillingPeriod` lia
o período sem tenant context — `billing_periods` tem FORCE RLS,
então a leitura nunca via a linha e "not found" virava 500 em vez
de 404. Corrigido exigindo `tenantId` explícito no reopen (igual ao
close) e lendo dentro de `withTenantContext`.
- [x] **Bug real, achado no teste desta fase**: `SubscriptionsController`
criava/lia `TenantSubscription` sem `withTenantContext` — RLS
rejeitava o create e o list sempre voltava vazio. Corrigido.
- [x] Teste unitário do `RatingEngine` (`packages/billing`, 17 casos,
`pnpm --filter @b2bcall/billing run test`) + testado ponta a ponta
contra a API real e Postgres real com RLS: período fechado com 8
`RatedUsageItem`s (chamada, 3 dias de ramal ativo, transcrição,
análise, tokens de entrada/saída) e total batendo exatamente com o
cálculo manual; fechar 2x = 409; reopen + reclose reusa os itens já
tarifados; reopen com tenant errado = 404 (RLS isolando de
verdade); tenant admin sem role de plataforma barrado (403) de
fechar mas lista as próprias statements normalmente;
`runActiveDaySweep` chamado 2x no mesmo dia sem duplicar
- [ ] **Lacuna real, conhecida**: `Call.calledNumber` ainda não é
populado pelo CDR (PHASE 17) — `CALL_SECONDS` sempre usa o fallback
plano (`CALL_MINUTE`), nunca o `RateDeck` por destino real
(`longestPrefixMatch`/`rateCallByDestination` só testados
isoladamente, não ponta a ponta)
- [ ] `RECORDING_BYTES` usa o storage atual no momento do fechamento como
proxy do período inteiro (sem histórico de tamanho por dia) —
decisão documentada em docs/BILLING.md, não uma média ponderada
- [ ] Reajuste de preço no meio de um período aberto (2 vigências
sobrepostas de `PriceBookItem`/`RateDeckEntry`) nunca exercitado
- [ ] Sem UI de platform admin pra criar tenant/price book/rate deck
ainda (fase Frontend) — testado só via API direto
## PHASE 23+ — ver `agente.md` seções 140 em diante (Frontend, Security,
Tests)
---