From 83eb2c5aeadf2f7b5a63df5c5ba91f90321a878c Mon Sep 17 00:00:00 2001 From: Matheus Date: Sun, 30 Aug 2026 17:10:15 -0300 Subject: [PATCH] fix(telefonia): tenant novo nasce sem conseguir discar entre ramais MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Achado real reportado pelo usuário: "a ligacao entre ramais nao esta funcionando". Não era regressão de nada desta sessão — o tenant onde o usuário registrou os próprios ramais de teste tinha ZERO regras de dialplan e ZERO versão ativa. Nenhum tenant nascia com a regra de "discagem interna": só existia nos tenants onde eu mesmo configurei manualmente durante testes anteriores (Acme). Todo tenant novo nascia incapaz de ligar entre os próprios ramais até alguém configurar isso à mão em Telefonia > Dialplan — um gap real de produto. `DEFAULT_DIALPLAN_EXTENSIONS` (novo, packages/telephony) tem as mesmas 2 regras já testadas ponta a ponta com chamada real (PHASE 53): discagem interna com pickup de grupo + `*8`. `TenantsController.create()` agora semeia essas regras e já ativa a versão 1 do contexto default, na mesma transação de criação do tenant. Reparo pontual aplicado no tenant existente do usuário (mesmas 2 regras, via SQL direto — não existe endpoint de "reparar tenant já criado"). Testado com uma chamada real: originate de um ramal pro outro tocou de verdade no aparelho do usuário (RINGING -> NO_ANSWER, não mais dialplan not found). Auto-seed testado criando um tenant novo de verdade via POST /tenants — confirmado no banco que a versão 1 já nasce ACTIVE. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8 --- TODO.md | 26 +++++++++++++++ apps/api/src/tenants/tenants.controller.ts | 34 +++++++++++++++++++- docs/DIALPLAN.md | 13 ++++++++ packages/telephony/src/default-dialplan.ts | 37 ++++++++++++++++++++++ packages/telephony/src/index.ts | 1 + 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/telephony/src/default-dialplan.ts diff --git a/TODO.md b/TODO.md index 4789c0e..557d91f 100644 --- a/TODO.md +++ b/TODO.md @@ -2091,6 +2091,32 @@ mostrar quantos ramais estao online e o status de cada ramal criado") hoje é construído à mão no editor genérico de dialplan; backend já suporta tudo que uma UI assim precisaria gerar +## PHASE 57 — Discagem interna não funcionava em tenant novo (achado real +reportado pelo usuário: "a ligacao entre ramais nao esta funcionando") +- [x] achado real: não era regressão nenhuma do trabalho desta sessão — + o tenant `teste01` (onde o usuário registrou 1501/1502 por conta + própria) tinha ZERO regras de dialplan e ZERO versão ativa. Nenhum + tenant nascia com a regra de "discagem interna" — só existia nos + tenants onde eu mesmo configurei manualmente durante os testes + (Acme). Ou seja: TODO tenant novo nascia sem conseguir ligar entre + os próprios ramais, um gap real de produto, não um bug pontual +- [x] `DEFAULT_DIALPLAN_EXTENSIONS` (novo, `packages/telephony`) — as + mesmas 2 regras já testadas ponta a ponta com chamada real na + PHASE 53 (discagem interna com pickup de grupo + `*8`). + `TenantsController.create()` agora semeia essas regras + já ativa + a versão 1 do contexto `default`, dentro da MESMA transação de + criação do tenant — nenhum tenant novo nasce mais sem conseguir + discar entre ramais +- [x] Reparo pontual do tenant `teste01` já existente (mesmas 2 regras, + inseridas direto via SQL — não tem endpoint de "reparar tenant + existente", só o seed de tenant NOVO). Testado com uma chamada + real: `originate` de 1501 pra 1502 tocou de verdade no aparelho do + usuário (`RINGING` → `NO_ANSWER`, não mais 404/dialplan not found) +- [x] Testado o auto-seed em si criando um tenant novo de verdade + (`testeivr`) via `POST /tenants` — confirmado no banco: versão 1 do + contexto `default` já nasce `ACTIVE` com as 2 regras. Tenant de + teste suspenso ao final (sem endpoint de delete de tenant) + --- ## Riscos conhecidos diff --git a/apps/api/src/tenants/tenants.controller.ts b/apps/api/src/tenants/tenants.controller.ts index 17bfcf4..c82542e 100644 --- a/apps/api/src/tenants/tenants.controller.ts +++ b/apps/api/src/tenants/tenants.controller.ts @@ -1,7 +1,8 @@ import { Body, Controller, ForbiddenException, Get, NotFoundException, Param, Patch, Post, UseGuards } from "@nestjs/common"; -import { getPrismaClient, withTenantContext, type TenantStatus } from "@b2bcall/database"; +import { getPrismaClient, withTenantContext, type Prisma, type TenantStatus } from "@b2bcall/database"; import { recordAuditEvent, isPlatformUser, hashPassword, type AccessTokenClaims } from "@b2bcall/auth"; import { generateStrongPassword } from "@b2bcall/shared"; +import { buildDialplanXml, DEFAULT_DIALPLAN_EXTENSIONS } from "@b2bcall/telephony"; import { JwtAuthGuard } from "../common/guards/jwt-auth.guard"; import { PermissionGuard } from "../common/guards/permission.guard"; import { RequirePermission } from "../common/decorators/require-permission.decorator"; @@ -75,6 +76,37 @@ export class TenantsController { const tenantAdminRole = await tx.role.findUniqueOrThrow({ where: { key: "tenant_admin" } }); await tx.userRole.create({ data: { userId: admin.id, roleId: tenantAdminRole.id, tenantId: tenant.id } }); + // Discagem interna + pickup de grupo (PHASE 57, docs/DIALPLAN.md) + // — achado real: nenhum tenant nascia com isso, então nenhum + // conseguia discar entre os próprios ramais até alguém configurar a + // regra manualmente em Telefonia > Dialplan. Mesmas 2 regras já + // testadas ponta a ponta com chamada real (PHASE 53). + for (const ext of DEFAULT_DIALPLAN_EXTENSIONS) { + await tx.dialplanExtension.create({ + data: { + tenantId: tenant.id, + context: "default", + name: ext.name, + conditionField: ext.conditionField, + conditionExpr: ext.conditionExpr, + actions: ext.actions as unknown as Prisma.InputJsonValue, + continueOnFalse: ext.continueOnFalse, + order: ext.order, + }, + }); + } + await tx.dialplanVersion.create({ + data: { + tenantId: tenant.id, + context: "default", + version: 1, + generatedXml: buildDialplanXml("default", DEFAULT_DIALPLAN_EXTENSIONS), + status: "ACTIVE", + createdByUserId: user.sub, + activatedAt: new Date(), + }, + }); + return { tenant, admin }; }); diff --git a/docs/DIALPLAN.md b/docs/DIALPLAN.md index 852de6f..9b0d503 100644 --- a/docs/DIALPLAN.md +++ b/docs/DIALPLAN.md @@ -28,6 +28,19 @@ POST /dialplan/versions/:id/activate -- marca ACTIVE, a anterior vira **Reativar uma versão antiga é o próprio rollback** — não existe endpoint separado. `GET /dialplan/versions` mostra o histórico completo. +## Dialplan padrão pra tenant novo (PHASE 57) + +Achado real reportado pelo usuário: "a ligação entre ramais não está +funcionando" — nenhum tenant nascia com nenhuma regra de dialplan, então +nenhum tenant novo conseguia discar entre os próprios ramais até alguém +configurar manualmente em Telefonia > Dialplan. `DEFAULT_DIALPLAN_EXTENSIONS` +(`packages/telephony/src/default-dialplan.ts`) tem as 2 regras mínimas +(discagem interna com pickup de grupo + `*8`, já testadas ponta a ponta +com chamada real na PHASE 53) — `TenantsController.create()` semeia essas +regras e já ativa a versão 1 do contexto `default`, dentro da MESMA +transação de criação do tenant. Um tenant criado ANTES desta fase precisa +de reparo manual (inserir as mesmas 2 linhas + gerar/ativar uma versão). + ## Mecanismo: dinâmico por chamada, não arquivo+rescan Ao contrário de Trunks (gateways carregados uma vez no boot/rescan), diff --git a/packages/telephony/src/default-dialplan.ts b/packages/telephony/src/default-dialplan.ts new file mode 100644 index 0000000..b3cb822 --- /dev/null +++ b/packages/telephony/src/default-dialplan.ts @@ -0,0 +1,37 @@ +import type { DialplanExtensionInput } from "./dialplan-xml"; + +/** + * Dialplan padrão do contexto "default" pra todo tenant novo (PHASE 57) + * — achado real: nenhum tenant nascia com isso, então nenhum tenant novo + * conseguia discar entre os próprios ramais até alguém configurar a + * regra manualmente em Telefonia > Dialplan. Mesmas 2 regras já testadas + * ponta a ponta com chamada real (PHASE 53): discagem interna com pickup + * de grupo (`callgroup`, `${user_data(...)}`) e `*8` pra capturar. + */ +export const DEFAULT_DIALPLAN_EXTENSIONS: DialplanExtensionInput[] = [ + { + name: "Capturar chamada do grupo (*8)", + conditionField: "destination_number", + conditionExpr: "^\\*8$", + continueOnFalse: false, + order: 5, + actions: [ + { application: "set", data: "my_callgroup=${user_data(${caller_id_number}@${domain_name} var callgroup)}" }, + { application: "pickup", data: "${my_callgroup}" }, + ], + }, + { + name: "Discagem interna", + conditionField: "destination_number", + conditionExpr: "^[0-9]{2,10}$", + continueOnFalse: false, + order: 10, + actions: [ + { + application: "set", + data: "called_party_callgroup=${user_data(${destination_number}@${domain_name} var callgroup)}", + }, + { application: "bridge", data: "user/${destination_number}@${domain_name},pickup/${called_party_callgroup}" }, + ], + }, +]; diff --git a/packages/telephony/src/index.ts b/packages/telephony/src/index.ts index 057daa7..0a82c1e 100644 --- a/packages/telephony/src/index.ts +++ b/packages/telephony/src/index.ts @@ -4,4 +4,5 @@ export * from "./freeswitch-provider"; export * from "./directory-xml"; export * from "./gateway-xml"; export * from "./dialplan-xml"; +export * from "./default-dialplan"; export * from "./queue-xml";