# 03 — Arquitetura — commerce-vc1

Fonte de verdade compartilhada: `docs/blueprint/_BRIEF.md`. Identificadores em inglês; prosa em português brasileiro. Fatos de legado só com caminho de arquivo; o que não foi lido no código está `A VERIFICAR`. Defaults ainda não decididos pelo operador estão `PADRÃO PROPOSTO` e reaparecem em `## Itens para 10-DECISIONS`.

Repo: `SoftwareBeesy/commerce-vc1` (fork de `sales-mework360`). Um deploy, multi-tenant por `Host`. Integração com produção via ports/adapters. Troca futura: `falepaco-api` + `falepaco-erp` + Kamailio (épico E6).

---

## 1. C4 nível 1 — contexto

O sistema é a **camada comercial** da VC1: atacado de voz (SIP trunk, DIDs, canais, minutos fixo/móvel Brasil) para revendas (Produto 01) e funil white-label da revenda até o cliente final (Produto 02). O switch e o ERP legado continuam donos do provisionamento telco até E6.

```mermaid
C4Context
  title commerce-vc1 — contexto (C4 L1)
  Person(staff, "Staff VC1", "Ops, finance, suporte")
  Person(reseller, "Usuário da revenda", "Owner, admin, finance, seller")
  Person(customer, "Cliente final", "Admin SIP / usuário")
  Person(prospect, "Prospecto", "PIN da proposta pública")

  System(commerce, "commerce-vc1", "Catálogo 2 níveis, funil, rating, crédito, fatura, painéis por Host")

  System_Ext(erp, "easy_cicerone", "WP + Slim + MySQL · api/white_label · AtivacaoTronco")
  System_Ext(sip, "SipPulse 4.2", "SOAP SubscriberWS/DidWS · OpenSIPS 1.11")
  System_Ext(cdr, "sippulse_reports", "MySQL cdrs_report · ~52k chamadas/dia")
  System_Ext(mepay, "mePay", "Cobranças boleto/Pix/cartão + NFCom via emissor terceiro")
  System_Ext(mesign, "meSign", "Envelopes + webhook HMAC")
  System_Ext(medns, "meDNS", "PowerDNS · zona vc1.com.br · ACME DNS-01")
  System_Ext(notify, "SMTP / SMS", "E-mail e SMS transacionais")
  System_Ext(site, "Site-VC1", "vc1.com.br institucional · fora de escopo")
  System_Ext(hub, "falepaco-api", "Hub futuro E6 · hoje fase 0, docs")

  Rel(staff, commerce, "admin.vc1.com.br Filament")
  Rel(reseller, commerce, "{slug}.vc1.com.br /painel")
  Rel(customer, commerce, "{slug}.vc1.com.br /app")
  Rel(prospect, commerce, "/p/{code} PIN 6 dígitos")
  Rel(site, commerce, "CTA seja revenda")
  Rel(commerce, erp, "ProvisioningPort")
  Rel(commerce, sip, "TelcoPort SOAP")
  Rel(commerce, cdr, "CdrPort SELECT")
  Rel(commerce, mepay, "PaymentPort + FiscalPort")
  Rel(commerce, mesign, "SignaturePort")
  Rel(commerce, medns, "DnsPort")
  Rel(commerce, notify, "NotificationPort")
  Rel(commerce, hub, "swap E6 · não-MVP")
```

Atores humanos não chamam SOAP nem Slim. Controllers Laravel só falam com ports.

---

## 2. C4 nível 2 — containers

Um cluster meCloud (Proxmox IDC-EVEO). LAB e PROD compartilham o mesmo desenho; LAB usa contas demo no legado com allowlist.

```mermaid
C4Container
  title commerce-vc1 — containers (C4 L2)
  Person(user, "Usuário HTTP")
  Container(edge, "nginx ou Caddy", "TLS wildcard ACME DNS-01", "Termina TLS; roteia Host; header X-Forwarded-Host")
  Container(api, "Laravel 12 API", "PHP 8.3 · Sanctum", "api.vc1.com.br + BFF da SPA · webhooks inbound")
  Container(filament, "Filament 4", "Painel staff", "admin.vc1.com.br · 2FA")
  Container(spa, "React 18 SPA", "Vite · TanStack Query · Tailwind", "/painel · /app · /p/{code}")
  Container(worker, "Queue workers", "Laravel Horizon", "rating, provisionamento, dunning, fiscal, outbox")
  Container(sched, "Scheduler", "cron Laravel", "import CDR, ciclo, limites, reconciliação")
  ContainerDb(pg, "PostgreSQL 16", "schemas identity…audit", "SoT comercial")
  ContainerDb(redis, "Redis 7", "cache · filas · locks", "Valkey-compatible")
  Container(obj, "Object storage", "S3-compatible", "PDFs, portabilidade, DANFE, contratos")
  System_Ext(legacy, "ERP + SipPulse + mePay + meSign + meDNS")

  Rel(user, edge, "HTTPS 443")
  Rel(edge, filament, "Host admin.*")
  Rel(edge, spa, "Host {slug}.* + cliente.* + sales.*")
  Rel(edge, api, "Host api.*")
  Rel(spa, api, "Sanctum cookie / Bearer")
  Rel(filament, pg, "Eloquent")
  Rel(api, pg, "Eloquent")
  Rel(api, redis, "cache/session")
  Rel(worker, redis, "queue")
  Rel(worker, pg, "jobs")
  Rel(sched, redis, "dispatch")
  Rel(api, obj, "upload/download")
  Rel(worker, obj, "PDFs")
  Rel(api, legacy, "adapters")
  Rel(worker, legacy, "adapters")
```

| Container | Papel | Notas |
|-----------|-------|-------|
| Edge (nginx **ou** Caddy) | TLS `*.vc1.com.br` via ACME DNS-01 contra meDNS; SNI; `X-Forwarded-Proto` / `X-Forwarded-Host` | `PADRÃO PROPOSTO`: Caddy (ACME nativo). Alternativa: nginx + certbot/lego. Decisão D-ARQ-01. |
| Laravel API | Contratos HTTP públicos, webhooks (`meSign`, `mePay`), BFF da SPA | Host `api.vc1.com.br` / `api.lab.vc1.com.br` |
| Filament | Único painel staff VC1 | Host `admin.vc1.com.br`. Path `PADRÃO PROPOSTO` `/` nesse Host (hoje o doador usa `/admin` em `AdminPanelProvider.php` L29). |
| React SPA | Revenda, cliente final, páginas públicas | Branding em runtime pelo `Host`; um bundle |
| Queue workers | Horizon sobre Redis | Separar filas `rating`, `provisioning`, `billing`, `fiscal`, `notify`, `outbox` |
| Scheduler | `php artisan schedule:run` a cada minuto | Tabela §7 |
| PostgreSQL 16 | SoT comercial; UUID `gen_random_uuid()`; dinheiro `*_cents bigint` | Schemas do brief |
| Redis 7 | Filas, cache, locks de rating/crédito, session Sanctum | `PADRÃO PROPOSTO` Redis; Valkey drop-in aceitável (alinhado a `falepaco-api` Decision #1) |
| Object storage | Arquivos de portabilidade (não pasta pública — R3), PDFs de proposta/contrato, DANFE | `PADRÃO PROPOSTO` bucket S3-compatible no meCloud (`meStorage`). Nunca URL previsível sem auth. |

Portas internas (`PADRÃO PROPOSTO`): API `8000`, SPA estático via edge, PG `5432`, Redis `6379`. Exposição pública só 443.

---

## 3. C4 nível 3 — módulos Laravel e hexágono

Bounded contexts = schemas PG do brief. Código hexagonal: domínio sem HTTP/SOAP; adapters atrás de ports.

### 3.1 Mapa contexto → schema → pasta

| Contexto | Schema PG | Pasta de domínio | Responsabilidade |
|----------|-----------|------------------|------------------|
| Identity | `identity` | `app/Domain/Identity` | `users`, `roles`, `user_roles`, `api_tokens`; staff / revenda / cliente |
| Tenancy | `tenancy` | `app/Domain/Tenancy` | `resellers`, `reseller_domains`, `reseller_branding`, `stfc_licenses`; resolução por Host |
| Pricing | `pricing` | `app/Domain/Pricing` | `price_bases`, `price_base_versions`, `vc1_tiers`, `reseller_tier_tables`, `reseller_tier_rows` |
| Catalog | `catalog` | `app/Domain/Catalog` | `plans`, `plan_versions`, `plan_quotas`, `suggested_packages` |
| Sales | `sales` | `app/Domain/Sales` | `proposals`, `proposal_events`, `contracts`, `signature_envelopes`; funil + PIN |
| Customers | `customers` | `app/Domain/Customers` | `customers`, `contacts`, `subscriptions`, `portability_requests`, `portability_files` |
| Telco | `telco` | `app/Domain/Telco` | `sip_accounts`, `dids`, `channel_allocations`, `block_events` (espelho; SoT switch = legado) |
| Rating | `rating` | `app/Domain/Rating` | `cdr_imports`, `usage_records`, `rated_calls`, `cycles`; cadência 30/6 |
| Billing | `billing` | `app/Domain/Billing` | `invoices`, `invoice_lines`, `statements`, `payments`, `credit_wallets`, `credit_ledger`, `credit_limits`, `limit_events`, `payouts`, `commission_invoices` |
| Fiscal | `fiscal` | `app/Domain/Fiscal` | `fiscal_documents`, `fiscal_attempts`; NFCom 62 |
| Integration | `integration` | `app/Domain/Integration` | `outbox`, `inbox_webhooks`, `connector_calls`, `legacy_id_map`, `reconciliation_runs`, `lab_allowlist` |
| Audit | `audit` | `app/Domain/Audit` | `audit_log` append-only |

### 3.2 Layout de pastas (`PADRÃO PROPOSTO`)

```
app/
  Domain/
    Identity/   Entities, ValueObjects, DomainEvents, Ports (interfaces)
    Tenancy/
    Pricing/
    Catalog/
    Sales/
    Customers/
    Telco/      Ports: ProvisioningPort, TelcoPort, CdrPort
    Rating/
    Billing/    Ports: PaymentPort
    Fiscal/     Ports: FiscalPort
    Integration/
    Audit/
    Shared/     MoneyCents, Destination, CycleId
  Application/  Use cases / command handlers (um por fluxo de §9)
  Http/
    Middleware/ ResolveTenantByHost, LabWriteGuard
    Controllers/
    Filament/
  Infrastructure/
    Adapters/
      LegacyErp/     LegacyErpProvisioningAdapter
      SipPulse/      SipPulseSoapAdapter
      SipPulseReports/ SipPulseReportsAdapter
      MePay/         MePayBillingAdapter + MePayFiscalAdapter (mesmo host/API mePay)
      MeSign/        MeSignAdapter
      MeDns/         MeDnsAdapter
      Notify/        SmtpMailAdapter, SmsAdapter
      Fake/          Fake*Adapter (Pest)
      Lab/           LabGuarded* decorator (allowlist)
    Persistence/     Eloquent models + global scopes por tenant
    Outbox/          OutboxDispatcher
```

Cada port tem: adapter PROD, `Fake*Adapter` para testes, decorator `LabGuarded*` que recusa write cujo `legacy_id` não está em `integration.lab_allowlist`.

Controllers **nunca** instanciam SoapClient nem chamam `api/white_label`. Só ports.

`ResellerWS` / `RatePlanWS` / `addCredit` do SipPulse **não** entram no desenho (existem e estão ociosos — `falepaco-sippulse-old/docs/AUDITORIA-SIPPULSE.md` §3.2). Rating e crédito vivem no PG.

---

## 4. Tenancy por Host

Tenant = revenda. Resolvido **só** pelo `Host` HTTP (nunca por path, nunca por token de usuário sozinho).

### 4.1 Resolução

Middleware `ResolveTenantByHost` (primeiro da stack web/api, depois `TrustProxies`):

1. Lê `Host` (ou `X-Forwarded-Host` se o proxy estiver na allowlist).
2. Normaliza FQDN minúsculo, sem porta.
3. Casa nesta ordem:
   - `admin.vc1.com.br` / `admin.lab.vc1.com.br` → contexto **staff**; `tenant_id = null`.
   - `api.vc1.com.br` / `api.lab.vc1.com.br` → contexto **api**; tenant vem do token Sanctum / API token (reseller bound).
   - `cliente.vc1.com.br` / `cliente.lab.vc1.com.br` → login genérico da revenda; após auth, **redirect 302** para `{slug}.vc1.com.br` (slug no `identity.users` → `tenancy.resellers.slug`).
   - `sales.vc1.com.br` → atalho de proposta; resolve `sales.proposals.code` e redireciona para `{slug}.vc1.com.br/p/{code}`.
   - `{slug}.vc1.com.br` / `{slug}.lab.vc1.com.br` → lookup `tenancy.reseller_domains` (`host` único) → `tenancy.resellers`.
   - Vanity futuro (`DnsPort`) entra na mesma tabela.
4. Slug reservado (lista do brief) → 404, nunca tenant.
5. Host desconhecido → 404 (não vaza existência de slug).

`PADRÃO PROPOSTO`: LAB usa o sufixo `.lab.vc1.com.br`; o slug é o mesmo da revenda demo.

### 4.2 Slugs reservados

`admin`, `cliente`, `api`, `sales`, `lab`, `www`, `sip`, `mail`, `status`, `docs`, `app`, `static`. Validação no cadastro da revenda e no `DnsPort`.

Domínio SIP: `A VERIFICAR` (per-reseller `sip.{slug}.vc1.com.br` vs domínio único legado `domain_sippulse` do `projetos_clientes`). Até decisão, o adapter usa `projetos_clientes.domain_sippulse` do projeto da revenda (coluna em `sql/fpn-592_create_table_projetos_clientes.sql` L12).

### 4.3 Session, cookie, CORS, Sanctum

| Superfície | Auth | Cookie / CORS |
|------------|------|----------------|
| Filament staff | sessão Filament + 2FA | cookie `Domain=admin.vc1.com.br` (host-only); `SameSite=Lax`; `Secure` |
| SPA revenda / cliente | Sanctum SPA cookie | cookie host-only do `{slug}.vc1.com.br`; **não** `.vc1.com.br` (evita vazamento cross-tenant) |
| Proposta pública | PIN 6 dígitos → cookie de acesso (padrão doador) | host-only; TTL `PADRÃO PROPOSTO` 12 h |
| API tokens revenda | `identity.api_tokens` Bearer | sem cookie; CORS só `https://{slug}.vc1.com.br` |
| Webhooks inbound | HMAC / assinatura do provedor | sem CORS |

`PADRÃO PROPOSTO` Sanctum `SANCTUM_STATEFUL_DOMAINS` = lista gerada dos hosts ativos + `admin` + `cliente` + `sales`. CORS allowlist idêntica; origem `*` proibida.

Sessão: `PADRÃO PROPOSTO` driver Redis, prefixo `vc1:{env}:`.

### 4.4 Scoping de queries

- Eloquent **global scope** `BelongsToReseller` em toda entidade de negócio (`customers.*`, `sales.*`, `catalog.plans` da revenda, `billing.*` com `invoice.party` respeitando o modo). Staff Filament usa `withoutGlobalScopes` + policy.
- `legacy_id` / `legacy_project_id` nunca autorizam sozinhos: o map `integration.legacy_id_map` é consultado **depois** do scope.
- Policies: `vc1_*` vê todos os tenants; `reseller_*` só o próprio; `customer_*` só `customers.customers.id` ligado ao user.
- Writes LAB: `LabGuarded*` consulta `integration.lab_allowlist` **antes** do adapter. Fora da lista → `LabWriteRefused` (não chama o legado).

### 4.5 Branding em runtime

Endpoint `GET /api/v1/tenancy/branding` (público, cache Redis 60 s, chave `branding:{host}`):

Resposta (`PADRÃO PROPOSTO`): `{ slug, trade_name, logo_url, primary_color, support_email, billing_mode_public_label }`. Sem dados fiscais, sem tokens.

A SPA lê no boot (`{slug}` e `/p/{code}`). Não há SCSS compilado por tenant (dívida do `wl_label` que **não** se copia).

---

## 5. Painéis e roteamento

| Host | UI | Rotas | Auth |
|------|----|-------|------|
| `admin.vc1.com.br` | Filament staff | `/` dashboard; resources de faixas, revendas, faturas, bloqueios, payouts, reconciliação | `vc1_admin`, `vc1_finance`, `vc1_ops`, `vc1_support` + 2FA |
| `{slug}.vc1.com.br` | React SPA | `/painel/*` revenda; `/app/*` cliente final; `/p/{code}` proposta pública | Sanctum / PIN |
| `cliente.vc1.com.br` | React login | `/painel/login` → redirect 302 para o host da revenda | ainda sem tenant |
| `sales.vc1.com.br` | redirect | `/p/{code}` → `{slug}.vc1.com.br/p/{code}` | — |
| `api.vc1.com.br` | JSON | `/api/v1/*`, `/api/v1/webhooks/*` | token / HMAC |
| `vc1.com.br` / `www` | Site-VC1 (repo separado) | fora deste app | — |

Doador `sales-mework360` (`frontend/src/App.tsx`): `/painel/login`, `/painel/propostas`, `/painel/vendas`, `/p/:slug`, `/sign/:uuid`. O fork **mantém** `/painel` e troca `/p/:slug` por `/p/{code}` (brief). `/sign/:uuid` do doador (cerimônia local) **não** é o caminho feliz: assinatura vai para meSign. `/app` é **novo**. Filament deixa de ser o painel do vendedor; vira só staff VC1.

Kanban da revenda (colunas do brief): `lead`, `negotiating`, `won`, `lost`. Stages internos da proposta: `draft`, `sent`, `viewed`, `trial_active`, `negotiating`, `accepted`, `pending_signature`, `contracted`, `expired`, `lost`, `cancelled`.

---

## 6. Autenticação e papéis

| Público | Mecanismo | 2FA | Notas |
|---------|-----------|-----|-------|
| Staff VC1 | Filament session | **obrigatório** (`PADRÃO PROPOSTO` TOTP; o doador **não** tem 2FA — `AdminPanelProvider.php` só `login()`) | Roles `vc1_admin`, `vc1_finance`, `vc1_ops`, `vc1_support` |
| Usuário da revenda | Sanctum SPA | `PADRÃO PROPOSTO` opcional TOTP para `reseller_owner` | Roles `reseller_owner`, `reseller_admin`, `reseller_finance`, `reseller_seller` |
| Usuário cliente final | Sanctum SPA no `/app` | não no MVP | Roles `customer_admin`, `customer_user` |
| Prospecto | PIN numérico 6 dígitos (`public_access_password` no doador — testes em `PublicProposalTest.php` exigem `/^\d{6}$/`) | — | Rate-limit por IP+código (já existe no doador) |
| API revenda | `identity.api_tokens` (Sanctum abilities) | — | Scopes `PADRÃO PROPOSTO`: `customers:read`, `invoices:read`, `dids:read` |
| Webhooks | HMAC do provedor | — | Inbox `integration.inbox_webhooks` antes do use case |

Senha padrão legado `"muda123"` em `Clientes.php` L192 **não** se copia. Controllers nunca aceitam total vindo do browser (R1). Token **nunca** na query string nas chamadas que o commerce origina (R2 do funil Intelbras); o legado ainda exige `token` em query/body — o adapter manda no body POST e header `PADRÃO PROPOSTO` `X-WL-Token`, caindo para query só se o Slim 3 ignorar o body em GET (`generate_login_link` é GET com `token` na query — `LinkLoginCliente.php` L15–21). GET legado permanece como está; POSTs novos usam body.

---

## 7. Jobs e schedules

| Job | Trigger | Fila | Idempotência |
|-----|---------|------|--------------|
| `ImportCdrJob` | schedule a cada **5 min** (`PADRÃO PROPOSTO`, D-ARQ-02) | `rating` | watermark `rating.cdr_imports.last_cdr_id` + `call_start_time` |
| `RateBatchJob` | após import (chain) | `rating` | `rated_calls.legacy_cdr_id` UNIQUE |
| `PrepaidDebitJob` | após rate | `rating` | ledger `usage_record_id` UNIQUE |
| `CreditCheckJob` | após debit + a cada 1 min se fila residual | `rating` | lock Redis `credit:{subscription_id}` |
| `LimitEventJob` | 80% / 100% pós-pago | `notify` | `billing.limit_events (subscription_id, threshold, cycle_id)` UNIQUE |
| `TelcoBlockJob` | saldo 0, limite 100%, overdue, manual | `provisioning` | `telco.block_events` + outbox |
| `CycleCloseJob` | diário 02:10 America/Sao_Paulo, fecha ciclo D-1 | `billing` | `rating.cycles.status` |
| `IssueInvoiceJob` | após cycle close, por modo | `billing` | `billing.invoices (subscription_id, cycle_id)` UNIQUE |
| `DunningJob` | diário 09:00 | `billing` | por fatura + dia |
| `FiscalRequestJob` | cliente clica **Nota fiscal** ou staff solicita (pós-`paid` ou fatura revenda emitida) | `fiscal` | `fiscal.fiscal_documents.invoice_id` + `Idempotency-Key` mePay |
| `FiscalDeliverJob` | webhook mePay `fiscal.issued` ou poll de fallback (`PADRÃO PROPOSTO` 15 min só se webhook atrasar) | `fiscal` | `mepay_fiscal_request_id`; `issued` só com chave 44 dígitos |
| `ReconciliationJob` | 04:00 | `default` | `integration.reconciliation_runs` |
| `PayoutCalcJob` | após pagamentos do ciclo (modo `vc1_invoices`) | `billing` | `billing.payouts (reseller_id, cycle_id)` |
| `StfcExpiryJob` | diário 08:00 | `notify` | alerta 30/7/0 dias; `reseller_invoices` sem validade → suspende **novas** vendas (`PADRÃO PROPOSTO`; contratos ativos não cortam no dia 0 — D-ARQ-03) |
| `OutboxDispatchJob` | a cada 5 s (Horizon) | `outbox` | `integration.outbox.id` + `published_at` |
| `DidProRataJob` | no add mid-cycle | `billing` | linha `invoice_lines` com `reason=pro_rata_did` |

Timezone `PADRÃO PROPOSTO`: `America/Sao_Paulo`. Ciclo = **mês calendário** `[1º 00:00, 1º mês seguinte 00:00)` para todas as revendas; `tenancy.resellers.due_day` (default 10) só vencimento da fatura VC1→revenda (D-DB-13).

---

## 8. Idempotência e outbox

- **Inbox webhooks** (`integration.inbox_webhooks`): dedupe `idempotency_key` = `provider + event_id` quando o provedor envia `event_id`; senão `sha256(envelope_id + event + payload)` (D-INT-23). Retransmissão retorna 200 sem reprocessar.
- **Outbox** (`integration.outbox`): gravado **na mesma transação PG** da mutação de domínio (padrão `falepaco-api` Decision #2). Colunas `PADRÃO PROPOSTO`: `id`, `event_type`, `aggregate_type`, `aggregate_id`, `payload jsonb`, `headers jsonb` (inclui `idempotency_key`), `status` (`pending`/`published`/`failed`), `attempts`, `available_at`, `published_at`.
- **Connector calls** (`integration.connector_calls`): toda chamada a adapter (sucesso ou falha) com `idempotency_key`, request hash, HTTP/SOAP status, duração. Serve métrica `integration.connector_calls` e reconciliação.
- Chave de idempotência outbound: `{use_case}:{aggregate_id}:{cycle_or_event}`. PaymentPort manda no header que o mePay definir (contrato § mePay em `06-INTEGRATIONS.md`).
- Workers: `ShouldBeUnique` Laravel no aggregate; lock Redis no rating **por subscription_id** (ordem `(started_at, legacy_cdr_id)` — D-ARQ-17).

Não há 2PC com MySQL do ERP. Sucesso comercial + falha legado = outbox retry. Falha comercial = rollback PG, legado não chamado. Ordem feliz de ativação: PG `subscription.status=trial|active` (pendente) → outbox `provisioning.requested` → adapter → `legacy_id_map` → `active`.

---

## 9. Observabilidade

| Sinal | Onde | Notas |
|-------|------|-------|
| Logs | JSON stdout → coletor meCloud | `request_id`, `tenant_id`, `user_id`, `legacy_id`; **nunca** token, senha SOAP, PIN, webhook_secret |
| Métricas | Prometheus (`PADRÃO PROPOSTO` `/metrics` interno) | `http_requests`, `queue_depth{queue}`, `rating_cdrs_imported`, `rating_lag_seconds`, `credit_blocks_total`, `integration.connector_calls{port,status}`, `fiscal_authorized_total` |
| Health | `GET /up` (doador já tem `/api/v1/health`) + `GET /api/v1/health/ready` | ready checa PG, Redis, disco do watermark CDR; **não** checa SipPulse (evita page se o SOAP oscilar) |
| Audit | `audit.audit_log` | actor, tenant, action, before/after resumido; bloqueio, preço abaixo do custo, payout, STFC |
| Tracing | `PADRÃO PROPOSTO` OpenTelemetry amostrado 1% PROD / 100% LAB | span por adapter |

Alertas mínimos: lag de CDR > 15 min; taxa de erro de adapter > 5%/5 min; fila `rating` > 10k; NFCom `rejected` > N; LAB write recusado (info).

---

## 10. Diagramas de sequência

### 10.1 Onboarding da revenda → projeto legado

Criar `projetos_clientes` **não** existe em `api/white_label` (só formulário WP `cadastrar_projeto_whitelabel.php`). O adapter usa um endpoint novo no legado (PR-LEG-02) ou insert mediado. Até o PR, o operador cria o projeto no ERP e cola o `legacy_project_id`.

```mermaid
sequenceDiagram
  actor Op as Operador VC1
  participant Fil as Filament admin.vc1
  participant PG as PG tenancy/pricing
  participant Out as outbox
  participant Erp as ProvisioningPort
  participant WL as api/white_label
  participant My as projetos_clientes

  Op->>Fil: Nova revenda (CNPJ, slug, billing_mode, faixa, STFC se reseller_invoices)
  Fil->>PG: INSERT resellers + reseller_tier_tables (cópia faixas VC1 + markup %)
  Fil->>PG: INSERT reseller_domains ({slug}.vc1.com.br)
  Fil->>Out: reseller.provision_requested
  alt PR-LEG-02 no ar
    Out->>Erp: createProject(nome, have_tronco=1, domain_sippulse, skip_tronco_billing=1)
    Erp->>WL: POST (novo) ou WP equivalente
    WL->>My: 1 linha tenant WL
    Erp-->>PG: legacy_id_map.project
  else MVP sem API de projeto
    Fil->>Op: cole o id do projetos_clientes já criado
    Op->>Fil: legacy_project_id
  end
  opt Tronco próprio da revenda (uso)
    Out->>Erp: cadastro_total_tronco + AtivacaoTronco
  end
  Fil->>Op: Host {slug}.vc1.com.br + users reseller_owner
```

Aprovação operador: projeto real em produção. Lab: só IDs na allowlist.

### 10.2 Proposta → trial → aceite → meSign → ativação

```mermaid
sequenceDiagram
  actor Rev as Revenda seller
  actor Cli as Prospecto
  participant Spa as SPA /painel /p/{code}
  participant API as Laravel
  participant PG as sales/customers
  participant Sign as SignaturePort meSign
  participant Prov as ProvisioningPort
  participant Tel as TelcoPort
  participant SIP as SipPulse SOAP
  participant ERP as easy_cicerone

  Rev->>Spa: wizard proposta (plano da revenda, marca runtime)
  Spa->>API: POST /api/v1/proposals
  API->>PG: proposals.stage=draft
  Rev->>API: emit + share → PIN 6 dígitos
  Cli->>Spa: GET /p/{code} + PIN
  opt trial do plano
    API->>Prov: cadastro_total_tronco (cliente + tronco)
    Prov->>ERP: POST /cadastro_total_tronco
    ERP-->>API: id_cliente (organizacoes.id)
    Note over ERP: AtivacaoTronco cron/job cria 750/850
    API->>Tel: insertSubscriber callLimit + insertDid
    Tel->>SIP: SubscriberWS.insertSubscriber / DidWS.insertDid
    API->>PG: subscription.status=trial, proposal.stage=trial_active
  end
  Cli->>API: POST .../accept
  API->>PG: accepted + Sale/contract
  API->>Sign: POST /api/v1/envelopes
  Sign-->>Cli: sign_url
  API->>PG: pending_signature
  Sign-->>API: webhook envelope.completed (HMAC)
  API->>PG: contracted
  API->>Prov: cadastro_total_tronco definitivo (se ainda não)
  API->>Tel: callLimit final + DIDs
  Tel->>SIP: updateSubscriberServices.callLimit / insertDid
  API->>PG: subscription.status=active, kanban won
```

`ProposalAcceptService` e `PacoSignatureFlow` do doador são reusados (tabela §11). Provisionamento MeWork (`StartPlatformOnboardingJob`) é **substituído** por `ProvisioningPort` + `TelcoPort`.

### 10.3 Import de CDR + rating + débito pré-pago + bloqueio

```mermaid
sequenceDiagram
  participant Sch as Scheduler 5 min
  participant Imp as ImportCdrJob
  participant My as sippulse_reports.cdrs_report
  participant PG as rating.*
  participant Rate as RateBatchJob
  participant Led as credit_ledger
  participant Tel as TelcoPort
  participant SIP as SubscriberWS

  Sch->>Imp: dispatch
  Imp->>My: SELECT id, call_start_time, accountcode, duration_block, callee_id, src_ip, service_type, price, rate WHERE id > watermark
  Imp->>PG: cdr_imports + usage_records (raw)
  Imp->>Rate: chain
  Rate->>Rate: 30/6; destino landline_br / mobile_br / other
  Rate->>PG: rated_calls (cost_cents faixa VC1, sell_cents plano)
  alt payment_mode=prepaid
    Rate->>Led: debit UNIQUE(usage_record_id)
    alt saldo <= 0
      Rate->>Tel: blockOutgoingCalls ou blockSubscriber
      Tel->>SIP: blockSubscriber(username=750{id}, domain)
      Rate->>PG: subscription.status=blocked_credit + block_events
    end
  else postpaid
    Rate->>PG: consumo vs credit_limit
    opt >= 80%
      Rate->>PG: limit_events 80 (alerta revenda + VC1)
    end
    opt >= 100%
      Rate->>Tel: block
      Rate->>PG: blocked_limit
    end
  end
```

Colunas reais do CDR: ver `06-INTEGRATIONS.md` `CdrPort`. Tarifar com **`duration`** (segundos atendidos); persistir **`duration_block`** como `switch_duration_block` só para reconciliação. Rating comercial **recalcula** 30/6 a partir de `duration`, sem reusar `price` do switch como venda (D-INT-01).

### 10.4 Fechamento mensal (os dois modos) + cobrança mePay + NFCom (via mePay)

```mermaid
sequenceDiagram
  participant Close as CycleCloseJob
  participant PG as billing/rating
  participant Pay as PaymentPort
  participant MP as mePay API
  participant Fis as FiscalPort

  Close->>PG: consumo = DIDs + canais (pró-rata) + minutos 30/6
  Close->>PG: charged = max(consumo, commitment) na faixa contratada
  alt billing_mode = vc1_invoices
    Close->>PG: invoice.party=customer preço da revenda
    Close->>Pay: upsertCustomer + POST /v1/invoices/adhoc
    Pay->>MP: adhoc invoice (Idempotency-Key)
    MP-->>Pay: mepay_invoice_id
    Close->>Pay: POST /v1/invoices/{id}/charges (boleto/Pix/card)
    Pay->>MP: charge + payment_url / Pix copia-e-cola
    Pay-->>PG: mepay_invoice_id, mepay_charge_id, payment_link_url
    Note over Fis: NFCom só após pagamento ou ação "Nota fiscal" (§10.7)
    MP-->>Close: webhook invoice.paid (§10.6)
    Close->>PG: payout = Σ(sell-cost) pagos - max(0, mínimo - custo_pagos - uso_próprio)
    alt payout >= 0
      Close->>PG: payouts pending_commission_invoice
    else payout < 0
      Close->>PG: invoice.party=reseller (diferença)
    end
  else billing_mode = reseller_invoices
    Close->>PG: statements por customer (sem NF ao cliente final)
    Close->>PG: invoice.party=reseller max(custo_filhos + uso_próprio, mínimo)
    Close->>Pay: adhoc + charge mePay (revenda paga via mePay — D-RM-16)
    Pay->>MP: invoice + charge link
    Pay-->>PG: refs mePay na fatura revenda
    Note over Fis: NFCom revenda solicitada após issue ou pós-paid (emissor RLGR atrás do mePay)
  end
```

Inadimplência em `vc1_invoices`: custo de faturas **não pagas** não conta no mínimo da revenda (`PADRÃO PROPOSTO` do brief, D-ARQ-04).

### 10.5 Pós-pago 80/100

```mermaid
sequenceDiagram
  participant Rate as CreditCheckJob
  participant PG as credit_limits / limit_events
  participant N as NotificationPort
  participant Tel as TelcoPort

  Rate->>PG: used_cents / credit_limit_cents
  alt cruzou 80% (uma vez por ciclo)
    Rate->>PG: limit_events threshold=80
    Rate->>N: e-mail revenda + staff VC1
  end
  alt cruzou 100%
    Rate->>PG: limit_events 100, subscription=blocked_limit
    Rate->>Tel: blockSubscriber
    Rate->>N: e-mail cliente + revenda + VC1
  end
```

### 10.6 Webhook mePay (pagamento) → baixa + desbloqueio

```mermaid
sequenceDiagram
  participant MP as mePay
  participant API as POST /api/v1/webhooks/mepay
  participant In as inbox_webhooks
  participant PG as payments / invoices
  participant Tel as TelcoPort
  participant SIP as SipPulse

  MP->>API: invoice.paid (Svix: webhook-id, webhook-timestamp, webhook-signature)
  API->>In: UNIQUE(mepay, webhook-id) + HMAC ≤5 min
  alt já processado
    API-->>MP: 200
  else novo
    API->>PG: billing.payments + invoice.status=paid (mepay_charge_id)
    alt subscription blocked_credit ou blocked_overdue ou blocked_limit resolvido
      API->>Tel: activateSubscriber
      Tel->>SIP: activateSubscriber
      API->>PG: subscription.active + block_events unblock
    end
    API-->>MP: 200
  end
```

Eventos mePay documentados: `invoice.created`, `invoice.paid`, `invoice.overdue`, `dunning.*`, `subscription.*` (`me360-mepay/docs/contracts/events.md`). VC1 trata `invoice.paid` como baixa; `invoice.overdue` alimenta dunning. Chargeback/refund: **a criar no mePay** ou confirmar via `POST /v1/invoices/{id}/confirm` + reconciliação. Chargeback → `blocked_manual` + alerta finance (`PADRÃO PROPOSTO`).

### 10.7 Cliente pede nota fiscal (NFCom via mePay)

```mermaid
sequenceDiagram
  actor Cli as Cliente final
  participant Spa as SPA /app/faturas
  participant API as Laravel API
  participant PG as fiscal.*
  participant Fis as FiscalPort
  participant MP as mePay API
  participant Iss as Emissor NFCom (terceiro)

  Cli->>Spa: Nota fiscal na fatura paga
  Spa->>API: POST /api/v1/app/invoices/{id}/fiscal-note
  API->>PG: fiscal_documents.status=requested (mepay_fiscal_request_id pendente)
  API->>Fis: requestFiscalNote(invoice_id, model=62, issuer_cnpj)
  Fis->>MP: POST /v1/fiscal-documents (a criar no mePay) + Idempotency-Key
  MP->>Iss: emissão NFCom modelo 62 (CNPJ emitente RLGR ou revenda conforme fatura)
  MP-->>API: 202 accepted + fiscalRequestId
  API->>PG: mepay_fiscal_request_id, status=requested
  API-->>Spa: solicitada
  MP-->>API: webhook fiscal.issued (a criar no mePay)
  API->>In: inbox dedupe
  API->>PG: access_key 44 dígitos, pdf/xml refs, status=issued
  API->>PG: outbox FiscalNoteIssued
  Cli->>Spa: Baixar nota
  Spa->>API: GET /api/v1/app/invoices/{id}/fiscal-note
  API-->>Spa: PDF/XML (files.objects ou URL assinada mePay)
```

VC1 **nunca** chama SyncNFe/Omie diretamente. Emitente legal (RLGR vs revenda) permanece conforme `issuer_cnpj` / `billing.invoices.issuer`; caminho técnico exclusivo mePay (ADR-013).

---

## 11. O que se reusa de `sales-mework360` e o que muda

Doador: `/home/beesy/Projetos/Cursor/suites/mework360/sales-mework360`. Stack: Laravel 12, Filament 4, React 18, Vite, TanStack Query, Tailwind, PostgreSQL 16, Redis, Sanctum, DomPDF, Pest, Vitest.

| Peça | Reuso | Mudança |
|------|-------|---------|
| Auth Sanctum SPA (`routes/api.php` `/auth/login`, `/auth/me`) | sim | multi-tenant por Host; papéis novos; cookie host-only |
| Filament (`AdminPanelProvider`, resources Users/Customers/Sales) | casca | vira painel **staff VC1**; resources de faixas/revendas/faturas; 2FA; path no Host admin |
| Wizard de proposta (`PanelNewProposalPage`, `newProposalWizard.ts`) | sim ~40–60% | catálogo de voz (canais, DIDs, franquia); preço da revenda; alerta margem negativa |
| Proposta pública `/p/{slug}` + PIN 6 dígitos | sim | `/p/{code}`; branding runtime; narrativa v2026 de voz |
| Folhas A4 v2026 + DomPDF | sim | conteúdo comercial de voz; marca da revenda |
| Trial/demo (`ProposalDemoStarter`) | fluxo | trial = subscriber SIP com franquia do plano, não tenant MeWork |
| `ProposalAcceptService` | sim | pós-aceite dispara `SignaturePort` + `ProvisioningPort`, não `StartPlatformOnboardingJob` |
| `PacoSignatureFlow` + `PacoAssignClient` | sim | `title`/`sender` VC1; `webhook_url` = `https://api.vc1.com.br/api/v1/webhooks/paco-assign` |
| `PacoAssignWebhookController` | sim | no `envelope.completed`, `ActivateProposalFromSignatureJob` chama TelcoPort; **não** `PlatformTenantActivationClient` |
| Kanban (`/proposals/kanban`) | sim | colunas lead/negotiating/won/lost; stages do brief |
| CEP/CNPJ públicos | sim | — |
| me360 e-mail, mailbox jobs, migration-requests | **não** | fora do produto voz |
| `SkipProvisioningGuard` / platform clients | **não** no caminho feliz | substituídos pelos ports |
| Checkout / mePay | não existia | **novo** `PaymentPort` |
| Multi-tenant Host / branding runtime | não existia | **novo** |
| Rating, crédito, payout, NFCom | não existia | **novo** |

Estimativa do plano: 40–60% do funil reaproveitável. O restante (tenancy, pricing 2 níveis, telco, billing) é código novo nos bounded contexts.

---

## 12. Performance — pipeline de rating

Fato legado (`falepaco-sippulse-old/docs/AUDITORIA-SIPPULSE.md` L31, L110): `sippulse_reports.cdrs_report` ≈ 8,8 M linhas, **≈ 52 mil chamadas/dia**, ≈ 85 mil min/dia (amostra 28–29/09).

Dimensionamento (`PADRÃO PROPOSTO`):

| Grandeza | Hoje | Folga 10× |
|----------|------|-----------|
| Chamadas/dia | ~52 k | 520 k |
| Média/min | ~36 | ~360 |
| Lote a cada 5 min | ~180 CDRs | ~1 800 |
| Pico 5 min (5× média) | ~900 | ~9 000 |

Pipeline:

1. `ImportCdrJob` faz `SELECT` keyset (`id > :watermark`) limitado a **2 000** linhas por tick (`PADRÃO PROPOSTO`). Se vier cheio, re-dispatch imediato.
2. Classificação de destino em PHP (prefixo BR fixo vs móvel) — CPU barata frente a I/O MySQL VPN.
3. `RateBatchJob` em chunks de 500; insert `rated_calls` em batch; UNIQUE em `legacy_cdr_id` permite retry.
4. Débito pré-pago: um `UPDATE … WHERE balance_cents >= :debit` por assinatura, serializado por lock Redis.
5. Workers Horizon: `PADRÃO PROPOSTO` 4 processos `rating` em PROD (1 em LAB).

Lag alvo: p95 < 10 min entre `call_start_time` e `rated_calls.rated_at`. Disco: 52 k × 365 ≈ 19 M `rated_calls`/ano — particionar por `cycle_id` (`PADRÃO PROPOSTO`).

O switch continua com 1 `rate_plan`; o valor `price` do CDR é **auditoria**, não venda.

---

## 13. Dívidas de segurança que **não** se copiam

- Total calculado no browser (R1, funis `wl_label` / VC1-Intelbras).
- Legado desligava TLS na SyncNFe (`faturamento.php` L9060) — adapters mePay **sempre** verificam TLS; emissor terceiro fica atrás do mePay.
- Token na URL nas chamadas **originadas** pelo commerce (GETs legado inevitáveis ficam isolados no adapter).
- Pasta pública de documentos de portabilidade (R3) — object storage privado + URL assinada.

---

## Itens para 10-DECISIONS

| ID | Item | Default |
|----|------|---------|
| D-ARQ-01 | Edge TLS: Caddy vs nginx+lego | `PADRÃO PROPOSTO`: Caddy com DNS-01 meDNS |
| D-ARQ-02 | Intervalo de import CDR | `PADRÃO PROPOSTO`: 5 minutos, lote 2 000 |
| D-ARQ-03 | STFC vencida: corta vendas novas ou também tráfego ativo? | `PADRÃO PROPOSTO`: só vendas novas no dia 0; tráfego segue até decisão finance |
| D-ARQ-04 | Inadimplência no mínimo da revenda (`vc1_invoices`) | `PADRÃO PROPOSTO` do brief: custo não pago **não** conta |
| D-ARQ-05 | Destino `other` (não fixo/móvel BR) | `PADRÃO PROPOSTO` do brief: bloqueado |
| D-ARQ-06 | Domínio SIP (`sip.{slug}.vc1.com.br` vs `domain_sippulse` único) | `A VERIFICAR`; até lá usar `projetos_clientes.domain_sippulse` |
| D-ARQ-07 | Object storage | `PADRÃO PROPOSTO`: S3-compatible meStorage |
| D-ARQ-08 | 2FA staff | `PADRÃO PROPOSTO`: TOTP Filament obrigatório |
| D-ARQ-09 | Path Filament no Host admin | `PADRÃO PROPOSTO`: `/` (não `/admin`) |
| D-ARQ-10 | Fallback `generate_login_link` → `painel_wl` no MVP | **Não** no go-live PROD (`/app` é E4). Só LAB E1–E3 com flag `legacy_painel_wl_link` **off** por default (ver D-RM-10) |
| D-ARQ-11 | Cookie/Sanctum host-only vs parent domain | `PADRÃO PROPOSTO`: host-only |
| D-ARQ-12 | Chargeback mePay | `PADRÃO PROPOSTO`: `blocked_manual` + alerta `vc1_finance` |
| D-ARQ-13 | Partição `rated_calls` | `PADRÃO PROPOSTO`: por `cycle_id` mensal |
| D-ARQ-14 | Workers Horizon `rating` em PROD | `PADRÃO PROPOSTO`: 4 processos |
| D-ARQ-15 | Timezone e hora do `CycleCloseJob` | `PADRÃO PROPOSTO`: `America/Sao_Paulo` 02:10 |
| D-ARQ-16 | Criação de `projetos_clientes` no onboarding | `PADRÃO PROPOSTO`: PR-LEG-02; MVP aceita colar `legacy_project_id` |
