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

View File

@@ -92,7 +92,7 @@ export async function persistCallEvent(normalized: NormalizedEvent): Promise<voi
});
if (normalized.type === "CALL_ENDED") {
await finalizeCall(tx, callId);
await finalizeCall(tx, callId, tenantId);
}
});
} catch (err) {
@@ -155,9 +155,10 @@ function buildPatch(normalized: NormalizedEvent): CallPatch {
/** Calcula os agregados em segundos (secao 155-156) uma vez que a chamada
* terminou — nunca antes, pra não gravar valores parciais. */
async function finalizeCall(tx: Prisma.TransactionClient, callId: string): Promise<void> {
async function finalizeCall(tx: Prisma.TransactionClient, callId: string, tenantId: string): Promise<void> {
const call = await tx.call.findUniqueOrThrow({ where: { id: callId } });
const talkTime = seconds(call.bridgeAt, call.endAt);
const billableSeconds = talkTime ?? 0;
await tx.call.update({
where: { id: callId },
@@ -166,7 +167,27 @@ async function finalizeCall(tx: Prisma.TransactionClient, callId: string): Promi
waitTime: seconds(call.queueEnterAt, call.agentAnswerAt),
talkTime,
durationSeconds: seconds(call.createdAt, call.endAt),
billableSeconds: talkTime ?? 0,
billableSeconds,
},
});
// "Chamada faturável" (agente.md secao 133) gera o UsageEvent que o
// RatingEngine (packages/billing) consome no fechamento do período —
// nunca calcula o valor aqui, só registra o fato bruto (segundos
// faturáveis). Chamada sem talk time (nunca bridgeou) não gera evento —
// nada a cobrar.
if (billableSeconds > 0) {
await tx.usageEvent.create({
data: {
tenantId,
callId,
meter: "CALL_SECONDS",
quantity: billableSeconds,
unit: "seconds",
sourceType: "call",
sourceId: callId,
occurredAt: call.endAt ?? new Date(),
},
});
}
}