# Plans / Entitlements Agente.md secao 56-62. Fase que ficou pra trás: a ordem de implementação (secao 232) coloca "Plans / Entitlements" logo depois de PostgreSQL RLS, bem antes de FreeSWITCH — mas o build seguiu direto pra FreeSWITCH sem essa peça, e toda fase desde então documentou "quota depende de Plans/ Entitlements" como pendência (EXTENSIONS.md, TRUNKS.md, AGENTS.md, QUEUES.md). Esta fase fecha essa lacuna antes de avançar pra Campanhas, que dependem de `max_campaigns`, e pro CPS Limiter, que depende de `max_cps`/ `max_concurrent_calls`. ## Modelo `plans`: catálogo compartilhado entre tenants (não é tenant-scoped, sem RLS — é um "menu" de planos, não dado de um tenant específico). Campos de limite (`max_extensions`, `max_agents`, `max_trunks`, `max_queues`, `max_campaigns`, `max_cps`, `max_concurrent_calls`, `max_daily_calls`, `max_monthly_calls`, `max_recording_storage_gb`) são todos `Int?` — **null significa sem limite**, nunca "sem plano". Campos de feature flag (`recording_enabled`, `ai_enabled`, `ai_transcription_enabled`, `ai_analysis_enabled`, `api_access_enabled`) são booleanos. `tenants.plan_id` é **obrigatório** (nunca null) — agente.md secao 56 pede explicitamente pra não espalhar `if plan == PRO` pelo código; em vez disso, todo tenant sempre tem um Plan de verdade pra ler, e o código só lê limites, nunca checa "qual plano é esse". Migration `plans_campaigns_leads`: cria a tabela `plans`, insere um plano "trial" seed, faz backfill de `plan_id` pros tenants já existentes (havia 2 tenants de teste no banco), só então torna a coluna `NOT NULL` — nessa ordem porque Postgres não deixa adicionar uma coluna `NOT NULL` sem default numa tabela não-vazia. ## `packages/entitlements` Pacote novo, dedicado (mesmo padrão de `packages/telephony`/`packages/ auth`). Duas funções: ```typescript assertQuota(tenantId, key: QuotaKey, currentCount: number): Promise // lança QuotaExceededError se currentCount >= limite do plano; no-op se o // campo for null (sem limite). Chamar ANTES de criar a linha. assertFeatureEnabled(tenantId, key: FeatureKey): Promise // lança FeatureNotEnabledError se o plano não habilita o recurso. ``` `DomainExceptionFilter` (apps/api) mapeia os dois erros pra 403 — mesmo padrão já usado pra `InvalidCredentialsError`/`NotATenantMemberError` (erros de domínio simples, sem depender de NestJS, traduzidos pra HTTP só na borda). ## Retrofit nos controllers existentes `ExtensionsController`, `TrunksController`, `AgentsController`, `QueuesController` e o novo `CampaignsController` agora contam as linhas ativas (`deletedAt: null`) antes de criar e chamam `assertQuota`. Sempre nessa ordem: contar → checar quota → só então criar — nunca criar e desfazer se estourar. ## Plano seed: "trial" ``` max_extensions: 5 max_agents: 5 max_trunks: 2 max_queues: 3 max_campaigns: 2 max_cps: 3 max_concurrent_calls: 5 max_daily_calls: 200 max_monthly_calls: 4000 max_recording_storage_gb: 1 recording_enabled: true, resto (ai_*) desabilitado ``` Números conservadores pra um plano de teste — o fluxo de "escolher/mudar de plano" de verdade é da fase Billing (secao 229). ## Verificado ponta a ponta ``` POST /extensions x5 -> 201 (dentro do limite de 5) POST /extensions x6 -> 403 "Quota excedida: maxExtensions (limite do plano: 5)" POST /campaigns x2 -> 201 (dentro do limite de 2) POST /campaigns x3 -> 403 "Quota excedida: maxCampaigns (limite do plano: 2)" ``` ## O que falta - Fluxo de escolha/upgrade de plano (fase Billing) — hoje todo tenant novo nasce com "trial" via o mesmo default do backfill; não há endpoint pra trocar de plano ainda. - `max_cps`/`max_concurrent_calls` só existem como campo lido — a aplicação em tempo real (rejeitar originate além do limite) é da fase CPS Limiter. - `max_daily_calls`/`max_monthly_calls`/`max_recording_storage_gb` — sem nenhum consumidor ainda (dependem de CDR/Recording, fases futuras).