feat(telefonia): rotas de entrada por DID — fundação real pro IVR
Pedido do usuário: "pode iniciar a montar o IVR e as rotas de entrada". Investigando antes de escrever qualquer XML de IVR, achei que NENHUMA chamada de entrada por tronco tinha como funcionar hoje, IVR ou não: nenhuma carregava `b2bcall_tenant_id` (só REGISTER de ramal e discagem de saída setam essa variable), e mesmo corrigindo isso, o profile "external" apontava pro contexto "public" vanilla — um arquivo ESTÁTICO, que sempre ganha de uma consulta ao mod_xml_curl, então nunca seria dinâmico enquanto se chamasse "public". Perguntei ao usuário a granularidade certa (por DID ou por tronco) antes de desenhar o schema — escolheu por DID, mais flexível (um tronco pode carregar vários números com destinos diferentes). `InboundRoute` nova (RLS real, FORCE ROW LEVEL SECURITY): `didNumber` único GLOBAL entre tenants (mesma exceção já aceita em Tenant.telephonyDomain) — é a ÚNICA forma de descobrir de qual tenant é uma chamada de entrada ANTES de identificar o tenant. Resolvido por fan-out sobre tenants ativos, nunca uma query sem contexto de RLS. Dockerfile repontou o profile external pra context="inbound" (sem arquivo estático, cai no mod_xml_curl de verdade). O XML gerado pra esse contexto injeta b2bcall_tenant_id + domain_name (achado real: sem setar domain_name explicitamente, o bridge da "Discagem interna" resolvia pro domínio GLOBAL default, nunca pro do tenant) e transfere pro dialplan real do tenant — reaproveita 100% do que já existe, inclusive pickup de grupo (PHASE 53). CRUD completo (InboundRoutesController, permissions novas no seed) + tela "Telefonia > Rotas de Entrada" no frontend. Testado com uma chamada REAL: um softphone registrado como ramal normal, outro discando direto pro profile external (porta 5080, sem registrar — exatamente como um provedor de tronco manda) um DID cadastrado. `show channels` confirma: tenant certo, contexto certo, domínio certo no bridge, codec PCMU negociado nos dois lados, ramal tocou e atendeu de verdade. Detalhes completos, inclusive uma tentativa de teste que falhou por limitação do canal `loopback` (não um bug), em docs/INBOUND_ROUTES.md. O IVR em si (menu com play_and_get_digits) fica pra próxima fase — esta é a fundação sem a qual nada de chamada de entrada funcionava. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
-- PHASE 56: rotas de entrada por DID (chamada de tronco -> tenant/destino)
|
||||
CREATE TABLE "inbound_routes" (
|
||||
"id" UUID NOT NULL,
|
||||
"tenant_id" UUID NOT NULL,
|
||||
"did_number" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"destination_context" TEXT NOT NULL DEFAULT 'default',
|
||||
"destination_number" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "inbound_routes_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "inbound_routes_did_number_key" ON "inbound_routes"("did_number");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "inbound_routes_tenant_id_idx" ON "inbound_routes"("tenant_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "inbound_routes" ADD CONSTRAINT "inbound_routes_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- Tabela de negocio tenant-scoped: RLS obrigatorio (ver docs/TENANT_ISOLATION.md).
|
||||
-- didNumber e' @unique GLOBAL de proposito (mesma excecao ja aceita em
|
||||
-- Tenant.telephonyDomain) -- e' a UNICA forma de descobrir de qual tenant
|
||||
-- e' uma chamada de entrada ANTES de identificar o tenant. A resolucao em
|
||||
-- apps/freeswitch-config faz fan-out sobre tenants ativos com
|
||||
-- withTenantContext (nunca uma query sem contexto), entao RLS continua
|
||||
-- valendo de verdade aqui, igual toda outra tabela de negocio.
|
||||
ALTER TABLE "inbound_routes" ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "inbound_routes" FORCE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "tenant_isolation" ON "inbound_routes"
|
||||
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|
||||
@@ -65,6 +65,7 @@ model Tenant {
|
||||
userRoles UserRole[]
|
||||
extensions Extension[]
|
||||
trunks Trunk[]
|
||||
inboundRoutes InboundRoute[]
|
||||
dialplanExtensions DialplanExtension[]
|
||||
dialplanVersions DialplanVersion[]
|
||||
queues Queue[]
|
||||
@@ -442,6 +443,43 @@ model Trunk {
|
||||
@@map("trunks")
|
||||
}
|
||||
|
||||
// Rota de entrada por DID (PHASE 56) — achado real: nenhuma chamada que
|
||||
// chega por um tronco carrega `b2bcall_tenant_id` hoje (só ramal
|
||||
// registrado e discagem de saída setam essa variable), então uma chamada
|
||||
// de entrada não tem como saber de qual tenant é. `didNumber` é a ÚNICA
|
||||
// forma de descobrir isso ANTES de identificar o tenant — por isso é
|
||||
// globalmente único entre TODOS os tenants (mesma exceção já aceita pra
|
||||
// `Tenant.telephonyDomain`, secao 52), nunca dois tenants podem reivindicar
|
||||
// o mesmo número. Resolvido por fan-out sobre tenants ativos em
|
||||
// `apps/freeswitch-config` (mesmo padrão de `updateTrunkStatusFromGatewayEvent`),
|
||||
// não por uma query sem RLS — ver docs/INBOUND_ROUTES.md.
|
||||
model InboundRoute {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
tenantId String @map("tenant_id") @db.Uuid
|
||||
|
||||
didNumber String @unique @map("did_number")
|
||||
description String?
|
||||
|
||||
// Contexto de dialplan do TENANT DONO que recebe a chamada depois da
|
||||
// resolução (ex.: "default" pra cair direto na discagem interna já
|
||||
// existente, ou um contexto de IVR dedicado) + o destination_number
|
||||
// sintético usado dentro dele (número de ramal real, ou um destino
|
||||
// reservado do menu de IVR).
|
||||
destinationContext String @default("default") @map("destination_context")
|
||||
destinationNumber String @map("destination_number")
|
||||
|
||||
enabled Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
@@map("inbound_routes")
|
||||
}
|
||||
|
||||
enum DialplanVersionStatus {
|
||||
DRAFT
|
||||
ACTIVE
|
||||
|
||||
Reference in New Issue
Block a user