import type { RateDeckEntryLike, PriceBookItemLike, CallRatingResult } from "./types"; function isValidAt(validFrom: Date, validUntil: Date | null, at: Date): boolean { return validFrom <= at && (validUntil === null || at < validUntil); } /** * Longest prefix matching (agente.md secao 129) — entre as entries do rate * deck válidas em `at`, retorna a de prefixo mais longo que `calledNumber` * começa com. Empate em tamanho de prefixo: indefinido qual vence (não * deveria acontecer com um rate deck bem configurado — dois prefixos * idênticos vigentes ao mesmo tempo é erro de cadastro, não algo pro * engine resolver silenciosamente). */ export function longestPrefixMatch( entries: RateDeckEntryLike[], calledNumber: string, at: Date, ): RateDeckEntryLike | null { let best: RateDeckEntryLike | null = null; for (const entry of entries) { if (!isValidAt(entry.validFrom, entry.validUntil, at)) continue; if (!calledNumber.startsWith(entry.prefix)) continue; if (!best || entry.prefix.length > best.prefix.length) best = entry; } return best; } /** * Item de price book vigente em `at` pra um `type` (agente.md secao 128). * Se mais de um item do mesmo tipo estiver vigente ao mesmo tempo (não * deveria, mas não é validado na escrita), pega o de `effectiveFrom` mais * recente — o reajuste mais novo vence. */ export function resolvePriceBookItem( items: PriceBookItemLike[], type: string, at: Date, ): PriceBookItemLike | null { let best: PriceBookItemLike | null = null; for (const item of items) { if (item.type !== type) continue; if (!isValidAt(item.effectiveFrom, item.effectiveUntil, at)) continue; if (!best || item.effectiveFrom > best.effectiveFrom) best = item; } return best; } /** * Chamada faturável (agente.md secao 133): aplica minimum_seconds (piso), * arredonda PRA CIMA pro próximo múltiplo de billing_increment_seconds * (nunca arredonda pra baixo — telecom sempre cobra o incremento cheio * iniciado), converte pra minutos fracionários e calcula o valor * (minutos * preço/minuto + taxa de conexão fixa). */ export function rateCallByDestination( billableSeconds: number, entry: RateDeckEntryLike, ): CallRatingResult { const flooredSeconds = Math.max(billableSeconds, entry.minimumSeconds); const increment = entry.billingIncrementSeconds > 0 ? entry.billingIncrementSeconds : 1; const roundedSeconds = Math.ceil(flooredSeconds / increment) * increment; const ratedMinutes = roundedSeconds / 60; const ratedAmount = ratedMinutes * entry.pricePerMinute + entry.connectionFee; return { matchedEntry: entry, billingIncrementSeconds: entry.billingIncrementSeconds, ratedMinutes, destinationRate: entry.pricePerMinute, ratedAmount, }; } /** * Fallback quando nenhum prefixo do rate deck bate (secao 129 não define * o que fazer nesse caso — decisão desta implementação: usa o * PriceBookItem(type=CALL_MINUTE) como tarifa plana genérica, sem * connection fee nem mínimo/incremento próprios — só arredonda pro * minuto cheio pra cima, a granularidade mais grosseira e mais segura * (nunca cobra a menos por falta de config). */ export function rateCallFlatFallback(billableSeconds: number, callMinuteItem: PriceBookItemLike): CallRatingResult { const ratedMinutes = Math.ceil(billableSeconds / 60); const ratedAmount = ratedMinutes * callMinuteItem.unitPrice; return { matchedEntry: null, billingIncrementSeconds: 60, ratedMinutes, destinationRate: callMinuteItem.unitPrice, ratedAmount, }; } /** Uso genérico já na mesma unidade do price book item (tokens de IA, * AI_ANALYSIS_CALL por request, etc.) — sempre quantidade * preço * unitário, nunca calculado ad hoc em outro lugar do código (agente.md * secao 130: "Nunca calcular billing no frontend", e por extensão, nunca * fora deste módulo). */ export function rateGenericUsage(quantity: number, unitPrice: number): number { return quantity * unitPrice; } /** EXTENSION_ACTIVE_DAY/AGENT_ACTIVE_DAY/TRUNK_ACTIVE_DAY → preço mensal * (EXTENSION_MONTH/AGENT_MONTH/TRUNK_MONTH) prorateado pelos dias do * período de billing — um recurso ativo o período inteiro paga o preço * cheio, ativo metade do período paga metade. */ export function rateActiveDaysProrated(activeDays: number, monthlyPrice: number, daysInPeriod: number): number { if (daysInPeriod <= 0) return 0; return activeDays * (monthlyPrice / daysInPeriod); } /** AI_TRANSCRIPTION_SECONDS → AI_TRANSCRIPTION_MINUTE: arredonda PRA CIMA * pro minuto cheio (mesma convenção de `rateCallByDestination` — nunca * cobra a menos por fração de minuto). */ export function rateTranscriptionSeconds(seconds: number, pricePerMinute: number): number { return Math.ceil(seconds / 60) * pricePerMinute; } /** RECORDING_BYTES → RECORDING_GB_MONTH. Simplificação conhecida: usa os * bytes armazenados no momento do fechamento do período como proxy do * consumo do mês inteiro (não faz média ponderada por dia armazenado) — * documentado em docs/BILLING.md, aceitável nesta fase por não haver * ainda um histórico de tamanho por dia pra fazer a média de verdade. */ export function rateRecordingBytes(bytes: number, pricePerGbMonth: number): number { const gigabytes = bytes / 1_000_000_000; return gigabytes * pricePerGbMonth; }