Files
B2BCall-dialer/packages/entitlements/src/index.ts
Matheus 91c0448dd4 feat(ai): pipeline assincrono — transcricao, analise, prompts (fase 20)
Sub-fase B do modulo de IA: novo servico apps/ai-worker (poll + FOR UPDATE
SKIP LOCKED) processa AIJob de TRANSCRIPTION/ANALYSIS disparados
automaticamente apos uma gravacao ficar disponivel, respeitando a cascata
de privacidade Tenant>Queue>Campaign e o entitlement do Plan. Transcricao
separa o WAV estereo em 2 canais (parser proprio, sem ffmpeg) e transcreve
cada perna independente; analise sempre redige dados sensiveis antes de
sair pro provider e valida o resultado contra o schema antes de persistir.
CRUD de AIPromptTemplate/AIPromptVersion em apps/api.

Testado ponta a ponta contra o worker real em Docker e Postgres real com
RLS (cascata de privacidade em 3 cenarios, WAV sintetico real no object
storage, claim/retry/dead-letter reais) — chamada de rede contra
OpenAI/Anthropic continua nunca exercitada (mesma restricao de rede desde
o Provider Layer). Detalhes em docs/AI_PIPELINE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
2026-08-28 16:39:38 -03:00

83 lines
2.6 KiB
TypeScript

import { getPrismaClient, type Plan } from "@b2bcall/database";
/**
* Entitlements por plano (agente.md secao 56-61): "criar sistema genérico...
* não espalhar `if plan == PRO` pelo código". Todo tenant tem exatamente um
* Plan (nunca null) — um campo de limite null significa "sem limite", nunca
* "sem plano".
*/
export class QuotaExceededError extends Error {
constructor(
public readonly quotaKey: QuotaKey,
public readonly limit: number,
) {
super(`Quota excedida: ${quotaKey} (limite do plano: ${limit})`);
this.name = "QuotaExceededError";
}
}
export class FeatureNotEnabledError extends Error {
constructor(public readonly featureKey: FeatureKey) {
super(`Recurso nao habilitado no plano: ${featureKey}`);
this.name = "FeatureNotEnabledError";
}
}
/** Campos de limite (contagem) do Plan — null = sem limite. */
export type QuotaKey =
| "maxExtensions"
| "maxAgents"
| "maxTrunks"
| "maxQueues"
| "maxCampaigns"
| "maxConcurrentCalls"
| "maxDailyCalls"
| "maxMonthlyCalls";
/** Campos booleanos de feature flag do Plan. */
export type FeatureKey =
| "recordingEnabled"
| "aiEnabled"
| "aiTranscriptionEnabled"
| "aiAnalysisEnabled"
| "apiAccessEnabled";
export async function getPlanForTenant(tenantId: string): Promise<Plan> {
const prisma = getPrismaClient();
const tenant = await prisma.tenant.findUniqueOrThrow({
where: { id: tenantId },
include: { plan: true },
});
return tenant.plan;
}
/**
* Lança QuotaExceededError se `currentCount` já atingiu o limite do plano
* pra `key`. Chamar ANTES de criar a linha (currentCount = contagem atual,
* sem contar a nova) — nunca depois, pra não criar e ter que desfazer.
*/
export async function assertQuota(tenantId: string, key: QuotaKey, currentCount: number): Promise<void> {
const plan = await getPlanForTenant(tenantId);
const limit = plan[key];
if (limit === null || limit === undefined) return;
if (currentCount >= limit) {
throw new QuotaExceededError(key, limit);
}
}
/** Lança FeatureNotEnabledError se o plano não habilita `key`. */
export async function assertFeatureEnabled(tenantId: string, key: FeatureKey): Promise<void> {
const plan = await getPlanForTenant(tenantId);
if (!plan[key]) {
throw new FeatureNotEnabledError(key);
}
}
/** Versão não-lançante de `assertFeatureEnabled` — pra decisões em
* background (ex.: "devo enfileirar este job de IA?") onde lançar não faz
* sentido, só pular silenciosamente. */
export async function isFeatureEnabled(tenantId: string, key: FeatureKey): Promise<boolean> {
const plan = await getPlanForTenant(tenantId);
return Boolean(plan[key]);
}