import { getPrismaClient, withTenantContext } from "@b2bcall/database"; import { createLogger } from "@b2bcall/shared"; import type { FreeSwitchTelephonyProvider } from "@b2bcall/telephony"; const logger = createLogger("b2bcall-fs-events"); /** * sofia::register/unregister/expire não carregam `b2bcall_tenant_id` (só * existe como channel variable de CHAMADA, nunca de REGISTER) — resolve * por domínio (from-host), único por tenant desde a PHASE 52/53. Mesmo * padrão de fan-out do `updateTrunkStatusFromGatewayEvent`, mas aqui o * domínio já identifica o tenant direto, sem precisar tentar cada um. */ export async function updateExtensionRegistrationStatus( user: string | undefined, host: string | undefined, registered: boolean, ): Promise { if (!user || !host) return; const prisma = getPrismaClient(); const tenant = await prisma.tenant.findFirst({ where: { telephonyDomain: host }, select: { id: true } }); if (!tenant) { logger.debug("nenhum tenant encontrado pro dominio do REGISTER", { host, user }); return; } const result = await withTenantContext(prisma, tenant.id, (tx) => tx.extension.updateMany({ where: { number: user, tenantId: tenant.id, deletedAt: null }, data: { registeredAt: registered ? new Date() : null }, }), ); if (result.count > 0) { logger.info("status de registro do ramal atualizado", { user, host, registered }); } } /** * Reconciliação ao conectar/reconectar no ESL (secao 55): sem isto, um * ramal que já estava registrado ANTES do fs-events subir (ou durante uma * queda do serviço) ficaria com `registered_at` desatualizado até o * próximo REGISTER natural do aparelho — minutos de atraso, dependendo do * `registration-expires` configurado no softphone. `show registrations` * é a fonte de verdade ao vivo: zera todo mundo do tenant primeiro, * depois marca só quem está de fato na lista agora. */ export async function reconcileExtensionRegistrations(provider: FreeSwitchTelephonyProvider): Promise { const prisma = getPrismaClient(); let raw: unknown; try { raw = await provider.getRegistrations(); } catch (err) { logger.error("falha ao consultar registrations pra reconciliacao", { error: String(err) }); return; } const rows = (raw as { rows?: Array<{ reg_user?: string; realm?: string }> } | undefined)?.rows ?? []; const registeredByDomain = new Map>(); for (const row of rows) { if (!row.reg_user || !row.realm) continue; if (!registeredByDomain.has(row.realm)) registeredByDomain.set(row.realm, new Set()); registeredByDomain.get(row.realm)!.add(row.reg_user); } const tenants = await prisma.tenant.findMany({ where: { status: "ACTIVE", telephonyDomain: { not: null } }, select: { id: true, telephonyDomain: true }, }); for (const tenant of tenants) { const registeredNumbers = Array.from(registeredByDomain.get(tenant.telephonyDomain!) ?? []); await withTenantContext(prisma, tenant.id, async (tx) => { await tx.extension.updateMany({ where: { tenantId: tenant.id, deletedAt: null, number: { notIn: registeredNumbers } }, data: { registeredAt: null }, }); if (registeredNumbers.length > 0) { await tx.extension.updateMany({ where: { tenantId: tenant.id, deletedAt: null, number: { in: registeredNumbers } }, data: { registeredAt: new Date() }, }); } }); } logger.info("reconciliacao de registrations de ramais concluida", { tenantsChecked: tenants.length }); }