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,12 @@
import { IsDateString, IsUUID } from "class-validator";
export class ClosePeriodDto {
@IsUUID()
tenantId!: string;
@IsDateString()
periodStart!: string;
@IsDateString()
periodEnd!: string;
}

View File

@@ -0,0 +1,22 @@
import { IsDateString, IsNumber, IsOptional, IsString, IsUUID, MaxLength, Min } from "class-validator";
export class CreatePlanVersionDto {
@IsUUID()
planId!: string;
@IsNumber()
@Min(0)
basePrice!: number;
@IsOptional()
@IsString()
@MaxLength(3)
currency?: string;
@IsDateString()
effectiveFrom!: string;
@IsOptional()
@IsDateString()
effectiveUntil?: string;
}

View File

@@ -0,0 +1,55 @@
import { IsArray, IsBoolean, IsDateString, IsIn, IsNumber, IsOptional, IsString, MaxLength, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
const PRICE_ITEM_TYPES = [
"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",
] as const;
export class CreatePriceBookItemDto {
@IsIn(PRICE_ITEM_TYPES)
type!: (typeof PRICE_ITEM_TYPES)[number];
@IsNumber()
unitPrice!: number;
@IsDateString()
effectiveFrom!: string;
@IsOptional()
@IsDateString()
effectiveUntil?: string;
}
export class CreatePriceBookDto {
@IsString()
@MaxLength(120)
name!: string;
@IsOptional()
@IsString()
@MaxLength(3)
currency?: string;
@IsOptional()
@IsBoolean()
isDefault?: boolean;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreatePriceBookItemDto)
items?: CreatePriceBookItemDto[];
}

View File

@@ -0,0 +1,60 @@
import { IsArray, IsBoolean, IsDateString, IsIn, IsInt, IsNumber, IsOptional, IsString, Max, MaxLength, Min, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
const DESTINATION_TYPES = ["FIXED", "MOBILE", "INTERNATIONAL"] as const;
export class CreateRateDeckEntryDto {
@IsString()
@MaxLength(20)
prefix!: string;
@IsString()
@MaxLength(80)
destinationName!: string;
@IsIn(DESTINATION_TYPES)
destinationType!: (typeof DESTINATION_TYPES)[number];
@IsNumber()
pricePerMinute!: number;
@IsOptional()
@IsInt()
@Min(1)
@Max(3600)
billingIncrementSeconds?: number;
@IsOptional()
@IsInt()
@Min(0)
@Max(3600)
minimumSeconds?: number;
@IsOptional()
@IsNumber()
@Min(0)
connectionFee?: number;
@IsDateString()
validFrom!: string;
@IsOptional()
@IsDateString()
validUntil?: string;
}
export class CreateRateDeckDto {
@IsString()
@MaxLength(120)
name!: string;
@IsOptional()
@IsBoolean()
isDefault?: boolean;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateRateDeckEntryDto)
entries?: CreateRateDeckEntryDto[];
}

View File

@@ -0,0 +1,23 @@
import { IsDateString, IsInt, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from "class-validator";
export class CreateSubscriptionDto {
@IsUUID()
tenantId!: string;
@IsUUID()
planVersionId!: string;
@IsOptional()
@IsDateString()
startedAt?: string;
@IsInt()
@Min(1)
@Max(28)
billingCycleAnchor!: number;
@IsOptional()
@IsString()
@MaxLength(3)
currency?: string;
}

View File

@@ -0,0 +1,11 @@
import { IsString, IsUUID, MaxLength, MinLength } from "class-validator";
export class ReopenPeriodDto {
@IsUUID()
tenantId!: string;
@IsString()
@MinLength(3)
@MaxLength(500)
reason!: string;
}