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

@@ -0,0 +1,321 @@
-- CreateEnum
CREATE TYPE "tenant_subscription_status" AS ENUM ('TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELED');
-- CreateEnum
CREATE TYPE "price_item_type" AS ENUM ('BASE_SUBSCRIPTION', 'EXTENSION_MONTH', 'AGENT_MONTH', 'TRUNK_MONTH', 'CALL', 'CALL_MINUTE', 'FIXED_MINUTE', 'MOBILE_MINUTE', 'INTERNATIONAL_MINUTE', 'AI_TRANSCRIPTION_MINUTE', 'AI_ANALYSIS_CALL', 'AI_INPUT_TOKEN', 'AI_OUTPUT_TOKEN', 'RECORDING_GB_MONTH');
-- CreateEnum
CREATE TYPE "destination_type" AS ENUM ('FIXED', 'MOBILE', 'INTERNATIONAL');
-- CreateEnum
CREATE TYPE "usage_meter" AS ENUM ('CALL_COUNT', 'CALL_SECONDS', 'EXTENSION_ACTIVE_DAY', 'AGENT_ACTIVE_DAY', 'TRUNK_ACTIVE_DAY', 'RECORDING_BYTES', 'AI_TRANSCRIPTION_SECONDS', 'AI_ANALYSIS_REQUEST', 'AI_INPUT_TOKENS', 'AI_OUTPUT_TOKENS');
-- CreateEnum
CREATE TYPE "billing_period_status" AS ENUM ('OPEN', 'CALCULATING', 'READY', 'CLOSED', 'REOPENED');
-- CreateEnum
CREATE TYPE "billing_statement_category" AS ENUM ('PLAN_BASE', 'EXTENSIONS', 'AGENTS', 'TRUNKS', 'CALLS', 'MINUTES', 'AI_TRANSCRIPTION', 'AI_ANALYSIS', 'AI_TOKENS', 'STORAGE', 'ADJUSTMENT');
-- AlterTable
ALTER TABLE "calls" ADD COLUMN "billing_increment_seconds" INTEGER,
ADD COLUMN "destination_rate" DOUBLE PRECISION,
ADD COLUMN "rated_amount" DOUBLE PRECISION,
ADD COLUMN "rated_minutes" DOUBLE PRECISION;
-- AlterTable
ALTER TABLE "tenants" ADD COLUMN "price_book_id" UUID,
ADD COLUMN "rate_deck_id" UUID;
-- CreateTable
CREATE TABLE "plan_versions" (
"id" UUID NOT NULL,
"plan_id" UUID NOT NULL,
"version" INTEGER NOT NULL,
"base_price" DOUBLE PRECISION NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'BRL',
"effective_from" TIMESTAMP(3) NOT NULL,
"effective_until" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "plan_versions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_subscriptions" (
"id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"plan_version_id" UUID NOT NULL,
"status" "tenant_subscription_status" NOT NULL DEFAULT 'ACTIVE',
"started_at" TIMESTAMP(3) NOT NULL,
"ends_at" TIMESTAMP(3),
"billing_cycle_anchor" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'BRL',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenant_subscriptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "price_books" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'BRL',
"is_default" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "price_books_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "price_book_items" (
"id" UUID NOT NULL,
"price_book_id" UUID NOT NULL,
"type" "price_item_type" NOT NULL,
"unit_price" DOUBLE PRECISION NOT NULL,
"effective_from" TIMESTAMP(3) NOT NULL,
"effective_until" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "price_book_items_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "rate_decks" (
"id" UUID NOT NULL,
"name" TEXT NOT NULL,
"is_default" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "rate_decks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "rate_deck_entries" (
"id" UUID NOT NULL,
"rate_deck_id" UUID NOT NULL,
"prefix" TEXT NOT NULL,
"destination_name" TEXT NOT NULL,
"destination_type" "destination_type" NOT NULL,
"price_per_minute" DOUBLE PRECISION NOT NULL,
"billing_increment_seconds" INTEGER NOT NULL DEFAULT 60,
"minimum_seconds" INTEGER NOT NULL DEFAULT 0,
"connection_fee" DOUBLE PRECISION NOT NULL DEFAULT 0,
"valid_from" TIMESTAMP(3) NOT NULL,
"valid_until" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "rate_deck_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "usage_events" (
"id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"call_id" UUID,
"meter" "usage_meter" NOT NULL,
"quantity" DOUBLE PRECISION NOT NULL,
"unit" TEXT NOT NULL,
"source_type" TEXT NOT NULL,
"source_id" TEXT,
"occurred_at" TIMESTAMP(3) NOT NULL,
"metadata" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "usage_events_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "rated_usage_items" (
"id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"usage_event_id" UUID,
"ai_usage_record_id" UUID,
"call_id" UUID,
"price_book_item_id" UUID,
"rate_deck_entry_id" UUID,
"quantity" DOUBLE PRECISION NOT NULL,
"unit_price" DOUBLE PRECISION NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'BRL',
"billing_period_id" UUID,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "rated_usage_items_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "billing_periods" (
"id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"period_start" TIMESTAMP(3) NOT NULL,
"period_end" TIMESTAMP(3) NOT NULL,
"status" "billing_period_status" NOT NULL DEFAULT 'OPEN',
"closed_at" TIMESTAMP(3),
"reopened_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "billing_periods_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "billing_statements" (
"id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"billing_period_id" UUID NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'BRL',
"subtotal" DOUBLE PRECISION NOT NULL,
"adjustments" DOUBLE PRECISION NOT NULL DEFAULT 0,
"total" DOUBLE PRECISION NOT NULL,
"generated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_statements_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "billing_statement_items" (
"id" UUID NOT NULL,
"tenant_id" UUID NOT NULL,
"billing_statement_id" UUID NOT NULL,
"category" "billing_statement_category" NOT NULL,
"description" TEXT NOT NULL,
"quantity" DOUBLE PRECISION,
"unit_price" DOUBLE PRECISION,
"amount" DOUBLE PRECISION NOT NULL,
CONSTRAINT "billing_statement_items_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "plan_versions_plan_id_version_key" ON "plan_versions"("plan_id", "version");
-- CreateIndex
CREATE INDEX "tenant_subscriptions_tenant_id_status_idx" ON "tenant_subscriptions"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "price_book_items_price_book_id_type_effective_from_idx" ON "price_book_items"("price_book_id", "type", "effective_from");
-- CreateIndex
CREATE INDEX "rate_deck_entries_rate_deck_id_valid_from_idx" ON "rate_deck_entries"("rate_deck_id", "valid_from");
-- CreateIndex
CREATE INDEX "usage_events_tenant_id_occurred_at_idx" ON "usage_events"("tenant_id", "occurred_at");
-- CreateIndex
CREATE INDEX "usage_events_tenant_id_meter_idx" ON "usage_events"("tenant_id", "meter");
-- CreateIndex
CREATE INDEX "rated_usage_items_tenant_id_billing_period_id_idx" ON "rated_usage_items"("tenant_id", "billing_period_id");
-- CreateIndex
CREATE INDEX "billing_periods_tenant_id_status_idx" ON "billing_periods"("tenant_id", "status");
-- CreateIndex
CREATE UNIQUE INDEX "billing_periods_tenant_id_period_start_period_end_key" ON "billing_periods"("tenant_id", "period_start", "period_end");
-- CreateIndex
CREATE INDEX "billing_statements_tenant_id_billing_period_id_idx" ON "billing_statements"("tenant_id", "billing_period_id");
-- CreateIndex
CREATE INDEX "billing_statement_items_billing_statement_id_idx" ON "billing_statement_items"("billing_statement_id");
-- AddForeignKey
ALTER TABLE "tenants" ADD CONSTRAINT "tenants_price_book_id_fkey" FOREIGN KEY ("price_book_id") REFERENCES "price_books"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenants" ADD CONSTRAINT "tenants_rate_deck_id_fkey" FOREIGN KEY ("rate_deck_id") REFERENCES "rate_decks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "plan_versions" ADD CONSTRAINT "plan_versions_plan_id_fkey" FOREIGN KEY ("plan_id") REFERENCES "plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_subscriptions" ADD CONSTRAINT "tenant_subscriptions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_subscriptions" ADD CONSTRAINT "tenant_subscriptions_plan_version_id_fkey" FOREIGN KEY ("plan_version_id") REFERENCES "plan_versions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "price_book_items" ADD CONSTRAINT "price_book_items_price_book_id_fkey" FOREIGN KEY ("price_book_id") REFERENCES "price_books"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rate_deck_entries" ADD CONSTRAINT "rate_deck_entries_rate_deck_id_fkey" FOREIGN KEY ("rate_deck_id") REFERENCES "rate_decks"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "usage_events" ADD CONSTRAINT "usage_events_call_id_fkey" FOREIGN KEY ("call_id") REFERENCES "calls"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_usage_event_id_fkey" FOREIGN KEY ("usage_event_id") REFERENCES "usage_events"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_ai_usage_record_id_fkey" FOREIGN KEY ("ai_usage_record_id") REFERENCES "ai_usage_records"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_call_id_fkey" FOREIGN KEY ("call_id") REFERENCES "calls"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_price_book_item_id_fkey" FOREIGN KEY ("price_book_item_id") REFERENCES "price_book_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_rate_deck_entry_id_fkey" FOREIGN KEY ("rate_deck_entry_id") REFERENCES "rate_deck_entries"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "rated_usage_items" ADD CONSTRAINT "rated_usage_items_billing_period_id_fkey" FOREIGN KEY ("billing_period_id") REFERENCES "billing_periods"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "billing_periods" ADD CONSTRAINT "billing_periods_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "billing_statements" ADD CONSTRAINT "billing_statements_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "billing_statements" ADD CONSTRAINT "billing_statements_billing_period_id_fkey" FOREIGN KEY ("billing_period_id") REFERENCES "billing_periods"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "billing_statement_items" ADD CONSTRAINT "billing_statement_items_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "billing_statement_items" ADD CONSTRAINT "billing_statement_items_billing_statement_id_fkey" FOREIGN KEY ("billing_statement_id") REFERENCES "billing_statements"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- RLS (agente.md secao 233, docs/TENANT_ISOLATION.md) — mesmo padrao do
-- resto do sistema: FORCE ROW LEVEL SECURITY + policy unica baseada em
-- app.current_tenant_id. price_books/price_book_items/rate_decks/
-- rate_deck_entries/plan_versions NAO tem RLS (catalogos globais da
-- plataforma, sem tenant_id, mesmo padrao ja usado por "plans").
ALTER TABLE "tenant_subscriptions" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "tenant_subscriptions" FORCE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "tenant_subscriptions"
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
ALTER TABLE "usage_events" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "usage_events" FORCE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "usage_events"
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
ALTER TABLE "rated_usage_items" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "rated_usage_items" FORCE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "rated_usage_items"
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
ALTER TABLE "billing_periods" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "billing_periods" FORCE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "billing_periods"
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
ALTER TABLE "billing_statements" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "billing_statements" FORCE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "billing_statements"
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
ALTER TABLE "billing_statement_items" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "billing_statement_items" FORCE ROW LEVEL SECURITY;
CREATE POLICY "tenant_isolation" ON "billing_statement_items"
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);