34ef408f0053ebe107235f7d69e7f53ac4bb8996
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 7b62ad3d82 |
feat(entitlements,campaigns): plans/quotas + campanhas, leads, lista de bloqueio
Fecha duas fases: Plans/Entitlements (agente.md secao 56-62), que tinha
ficado pra trás desde o inicio, e Campanhas/Leads/Lista de Bloqueio (secao
63-71).
## Plans/Entitlements
A ordem de implementacao da propria especificacao (secao 232) coloca
Plans/Entitlements logo depois de PostgreSQL RLS, bem antes de FreeSWITCH
— mas o build seguiu direto sem essa peca, e toda fase desde entao
documentou "quota depende de Plans/Entitlements" como pendencia
(EXTENSIONS.md, TRUNKS.md, AGENTS.md, QUEUES.md, agora todas atualizadas).
Fechado agora porque Campanhas precisa de max_campaigns e o proximo CPS
Limiter vai precisar de max_cps/max_concurrent_calls.
- plans: catalogo compartilhado entre tenants (sem RLS, nao e' tenant-
scoped) com todos os campos de entitlement da secao 56. Campo de limite
null = "sem limite", nunca "sem plano" — tenants.plan_id e' obrigatorio,
nunca null (secao 56: nao espalhar `if plan == PRO` pelo codigo).
- Migration hand-escrita: cria plans, insere seed "trial", faz backfill de
plan_id pros tenants ja existentes, so' depois torna NOT NULL (Postgres
nao deixa NOT NULL sem default em tabela nao-vazia).
- packages/entitlements (pacote novo): assertQuota/assertFeatureEnabled,
erros mapeados pra 403 no DomainExceptionFilter.
- Retrofit em Extensions/Trunks/Agents/Queues: contam linhas ativas e
checam quota antes de criar.
## Campanhas, Leads, Lista de Bloqueio
Deliberadamente so' o modelo/CRUD/maquina de estados — o motor que de fato
origina chamadas (PredictiveDialerEngine, secao 72-86: dados em tempo
real, EWMA, CPS distribuido, reserva atomica de lead, lock de campanha,
bgapi originate, controle de abandono, retry) e' um sistema grande o
suficiente pra merecer fase propria (secao 72: "nao e' so' `for lead ->
originate`").
- campaigns/leads/suppression_entries (tenant-scoped, RLS).
- Maquina de estados da campanha (secao 64-66): start/pause/drain/stop com
tabela de transicoes validas — transicao invalida retorna 400, nunca
ignora silenciosamente. Apagar bloqueado enquanto RUNNING/DRAINING.
- packages/shared/src/phone.ts (secao 70): normalizacao dedicada,
preparada pra E.164 completo, so' BR implementado.
- Importacao CSV em batches de 1000 (secao 69): detecta duplicado (dentro
do CSV + contra leads existentes), checa lista de bloqueio (importa como
DO_NOT_CALL, nao descarta), retorna {total, valid, invalid, duplicates,
imported, suppressed}.
- Lista de bloqueio (secao 71): CRUD tenant-scoped.
Verificado ponta a ponta: campanha com queueId/trunkId invalido e
pacingMin > pacingMax rejeitados; CSV de 5 linhas (1 invalida, 1
duplicada, 1 bloqueada) importado corretamente; start->pause->drain->stop
e transicoes invalidas todas corretas; 3a campanha rejeitada por quota
(max_campaigns=2 do plano trial); 6a extensao rejeitada por quota
(max_extensions=5). Suites de teste existentes (tenant-isolation, auth)
atualizadas pro novo Tenant.planId obrigatorio e passando.
typecheck do workspace inteiro limpo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
|
|||
| 0720a0efe3 |
feat: implement Dialplan with structured editor and versioning
- dialplan_extensions table (tenant-scoped, RLS): structured editor per agente.md secao 43 -- context, condition field/expr, actions/anti-actions (JSON), continue, order, enabled. One condition per extension (deliberate simplification vs raw FreeSWITCH's multi-condition extensions). - dialplan_versions table (tenant-scoped, RLS): generate/validate/version/ activate flow (secao 44). Reactivating an older version IS the rollback mechanism -- no separate endpoint needed. - apps/api/src/dialplan: extensions CRUD + versions/generate (builds XML, validates well-formedness with fast-xml-parser, saves as DRAFT) + versions/:id/activate (atomically flips ACTIVE, supersedes the previous one). Reused freeswitch.view/.configure permissions rather than inventing new ones not in the agente.md permission list. - packages/telephony: buildDialplanXml() plus ALLOWED_DIALPLAN_APPLICATIONS, an explicit allowlist (answer/bridge/playback/hangup/set/export/... -- deliberately no system/exec/socket) guarding against a tenant configuring a dialplan action that runs arbitrary commands on the FreeSWITCH host (agente.md secao 180) - b2bcall-fs-config resolves dialplan dynamically per call (unlike Trunks' file+rescan approach -- dialplan is fetched fresh via mod_xml_curl on every call anyway) by tenant id from the variable_b2bcall_tenant_id channel variable already injected at directory resolution, then serving whichever DialplanVersion is ACTIVE for that context - verified end-to-end: created a rule for destination_number 7000, generated and activated v1, originated a call that actually routed through the dialplan (not bypassing it via &app()) -- CALL_CREATED -> CALL_ANSWERED -> CALL_ENDED with the correct tenantId throughout. Created and activated a v2, then rolled back to v1 by reactivating it; status transitions (ACTIVE/SUPERSEDED) all confirmed via the API. CRITICAL FINDING, fixed in this same phase: deliberately testing that the application allowlist rejects 'system' got back 201 instead of 400 -- NestJS's ValidationPipe had been silently inert across all of apps/api's @Body() DTOs since the API was first created. Root cause: running via (esbuild) instead of a real build -- esbuild doesn't always resolve cross-file parameter types for design:paramtypes metadata, and Nest skips validation without any error when it can't determine the DTO class. Fixed by always building with tsc before running (tsc && tsx dist/main.js -- still via tsx because internal workspace packages aren't built to JS yet). Re-verified with two deliberate bad-input tests post-fix, both correctly rejected with 400. A stray malicious test row (dialplan action 'system') created while the bug was live was deleted; it was never baked into an activated version, so nothing could have executed it. See docs/VALIDATION_PIPE_BUG.md for the full writeup. docs/DIALPLAN.md, docs/VALIDATION_PIPE_BUG.md, docs/EXTENSIONS.md updated |
|||
| c03c6d4eaa |
feat: implement Extensions with real FreeSWITCH directory integration
- extensions table (tenant-scoped, RLS): number, sip_password_enc
(AES-256-GCM via packages/shared/src/crypto.ts), caller_id, context,
sofia_profile, codecs, max_registrations
- apps/api/src/extensions: CRUD (POST/GET/GET:id/DELETE), protected by a
new generic PermissionGuard (@RequirePermission decorator), tenant
resolved only from the JWT (never trusted from the client)
- SIP password is returned in plaintext only once, in the create response;
toPublicExtension() explicitly destructures the encrypted field out
(not a spread) so it can't leak by accident
- b2bcall-fs-config now resolves real directory data: Tenant.telephonyDomain
-> Extension.number, decrypts the password, builds proper directory XML
including a dial-string param (missing it caused originate to fail with
MANDATORY_IE_MISSING instead of the expected USER_NOT_REGISTERED)
- pinned FreeSWITCH's 357737{domain} to a stable value (b2bcall.local) via a
vars.xml patch in the Dockerfile -- it previously used the container's
dynamic IP, which could never match a stored telephony_domain
- added HTTP Basic auth between FreeSWITCH and fs-config
(gateway-credentials, timingSafeEqual comparison) now that the service
returns real secret data, closing the gap flagged as pending in the XML
Curl phase instead of leaving it open
- found and fixed: PermissionGuard's constructor-injected Reflector came
back undefined at runtime under tsx/esbuild (unreliable cross-file
decorator metadata emission) -- fixed with an explicit @Inject(Reflector);
worth watching for in future guards/services run via tsx
- verified end-to-end: create extension -> originate user/<ext> reports
USER_NOT_REGISTERED (found, not registered) -> delete -> back to
SUBSCRIBER_ABSENT (not found); password never reappears in any GET;
unauthenticated fs-config requests get 401
- docs/EXTENSIONS.md
|