import { getPrismaClient, withTenantContext, type Tenant, type Plan } from "@b2bcall/database"; import { isFeatureEnabled } from "@b2bcall/entitlements"; import { resolvePrivacyLevel, allowsTranscription, type AIPrivacyLevelName } from "@b2bcall/ai"; import { createLogger } from "@b2bcall/shared"; const logger = createLogger("b2bcall-fs-events"); /** * Decide se uma gravação recém-disponível deve virar um AIJob de * transcrição (agente.md secao 104-108: "pipeline sempre assíncrono, * disparado depois que a gravação existe"). Duas checagens independentes, * as duas precisam passar: * * 1. Entitlement do Plan (`aiEnabled` + `aiTranscriptionEnabled`) — "essa * conta contratou IA?", nada a ver com o que o tenant CONFIGUROU. * 2. Cascata de privacidade Tenant > Queue > Campaign (secao 122) — "esse * tenant/fila/campanha específica AUTORIZOU processar esta chamada?". * * Roda só uma vez, no momento em que a gravação é confirmada — não há * reconsulta depois (se a config mudar depois, não afeta jobs já * decididos, mesma filosofia de "decisão tomada no momento do evento" já * usada pro resto do CDR). */ export async function enqueueTranscriptionJobIfEligible( tenantId: string, callId: string, tenant: Tenant & { plan: Plan }, queueId: string | null, campaignId: string | null, ): Promise { try { const [aiEnabled, transcriptionEnabled] = await Promise.all([ isFeatureEnabled(tenantId, "aiEnabled"), isFeatureEnabled(tenantId, "aiTranscriptionEnabled"), ]); if (!aiEnabled || !transcriptionEnabled) return; const prisma = getPrismaClient(); let queueLevel: AIPrivacyLevelName | null = null; let campaignTranscriptionEnabled = false; let campaignAnalysisEnabled = false; await withTenantContext(prisma, tenantId, async (tx) => { if (queueId) { const queue = await tx.queue.findFirst({ where: { id: queueId } }); queueLevel = (queue?.aiPrivacyLevel as AIPrivacyLevelName | null) ?? null; } if (campaignId) { const campaign = await tx.campaign.findFirst({ where: { id: campaignId } }); campaignTranscriptionEnabled = campaign?.aiTranscriptionEnabled ?? false; campaignAnalysisEnabled = campaign?.aiAnalysisEnabled ?? false; } }); const effectiveLevel = resolvePrivacyLevel({ tenantLevel: tenant.aiPrivacyLevel as AIPrivacyLevelName, queueLevel, campaignTranscriptionEnabled, campaignAnalysisEnabled, }); if (!allowsTranscription(effectiveLevel)) return; await withTenantContext(prisma, tenantId, (tx) => tx.aIJob.create({ data: { tenantId, callId, type: "TRANSCRIPTION" } }), ); logger.info("job de transcricao de IA enfileirado", { callId, effectiveLevel }); } catch (err) { logger.error("falha ao decidir/enfileirar job de IA", { error: String(err), callId }); } }