# 06 — Integrações — commerce-vc1

Contratos dos ports do brief. Fatos de legado citados com arquivo e linha. Segredos: só **nome** da variável / caminho de vault — nunca valores. Arquivos `.env`, `passw/`, secrets **não** foram lidos.

Cada adapter PROD tem `Fake*Adapter` (Pest) e decorator `LabGuarded*` (`integration.lab_allowlist`). Toda chamada grava `integration.connector_calls`. LAB recusa write fora da allowlist **antes** do transporte.

---

## Convenções comuns

| Tema | Default |
|------|---------|
| Idempotência outbound | header ou campo `idempotency_key` = `{use_case}:{aggregate_id}:{cycle_or_event}` |
| Timeouts | ver cada port; `PADRÃO PROPOSTO` connect 3 s + total 15 s HTTP, 20 s SOAP |
| Retry | HTTP 408/429/5xx e SOAP fault de transporte: 5 tentativas, backoff `30, 120, 600, 1800, 3600` s (mesmo padrão meSign `SendWebhookJob`). 4xx de negócio: **não** retry |
| Error mapping | envelope `{ error: { code, message } }` sem stack. Códigos: `legacy_unavailable`, `legacy_rejected`, `lab_write_refused`, `not_found`, `conflict`, `fiscal_not_authorized` |
| Observabilidade | `integration.connector_calls{port,operation,status}` + log JSON sem secrets |
| LAB | `APP_ENV=lab` → decorator; allowlist de `legacy_project_id`, `legacy_org_id`, usernames SIP, DIDs |

TLS: verificar peer. O legado desliga (`CURLOPT_SSL_VERIFYPEER, false` em `faturamento.php` L9060) — **não** copiar.

---

## 1. `ProvisioningPort` → `LegacyErpProvisioningAdapter`

**Propósito.** Criar/atualizar o espelho legado da revenda (1 `projetos_clientes`) e do cliente final (`organizacoes` + `projetos_clientes_tronco` + planos/DIDs), gerar link de painel, bloquear no ERP (que dispara SipPulse). SoT comercial é o PG; o ERP é backend de provisionamento.

**Alvo.** `https://erp.falepaco.com.br/easy-adapt/api/white_label` (`A VERIFICAR` path exato em PROD; clone em `/home/beesy/Projetos/Cursor/suites/falepaco/easy_cicerone/api/white_label/index.php`). Host legado `177.11.49.9` / `erp.falepaco.com.br` (brief).

**Transporte.** HTTP JSON Slim 3. `Content-Type: application/json`.

**Auth.** Token de projeto: constante `TOKEN_WHITE_LABEL` concatenada com `projetos_clientes.token` (`VerifyDataProject.php` L16–26). Aceito em query **ou** body (`index.php` L16–18). Segredo: vault/env `LEGACY_WL_TOKEN_PREFIX` (o prefixo) + por revenda `tenancy.resellers` → map `legacy_project_token` cifrado. **Nunca** logar o token. O commerce, em POST, manda no body; GET `generate_login_link` exige query (`LinkLoginCliente.php` L15–21).

### 1.1 Operações (rotas reais)

Bootstrap Slim: `api/white_label/index.php` L25–39.

| Operação port | Método / rota | Request (campos lidos no código) | Response |
|---------------|---------------|----------------------------------|----------|
| `registerTrunkCustomer` | `POST /cadastro_total_tronco` `rotas/CadastroTotalTronco.php` L7 | Body: `dados_cliente` (ver abaixo), `planos[]`, `id_fidelidade`, `id_vencimento`, `url_contrato`, `id_cliente` opcional. Token em query `TOKEN`. Handler injeta `id_projeto_cliente` (L39–42) e chama `Cadastro::Tronco()` (`controllers/Cadastro.php` L123–140). | `status`, `message`, `body[0]` com `id_cliente`, `planos[].id_plano`, `dids` (`Cadastro.php` L139, L430). Erro: `projetos_clientes_log_erro` + `id_log_erro` (L52–62). |
| `generateLoginLink` | `GET /generate_login_link` `rotas/LinkLoginCliente.php` L6 | Query: `token`, `id_cliente` (L15). | `status=success`, `body.link` = `{url_painel_pabx}/login/erp?auth={base64 id}&wl_token={token_painel_pabx}` (L41–43). Erros: dados incompletos, cliente não encontrado, projeto sem `url_painel_pabx`. |
| `setBlockStatus` | `POST /alterar_status_boqueio` (sic) `rotas/AlterarStatusCliente.php` L7 | Query `TOKEN`. Body: `status` (0/1), `id_cliente` (L34–35). | `success` / `info` (já no estado) / `error` (nunca desbloquear / falha). ERP chama SipPulse se parâmetro `INT_SIP_PULSE` (`organizacao.php` L7028–1050). Tronco: `Bloqueia_Desbloqueio_Clientes_Tronco` prefixos 750 e 850 (L1046–1047). |
| `listDids` | `GET /dids/disponiveis` `rotas/ManageDidsRoute.php` L10 | Query: `codigo_area` (ou `CODIGO_AREA`), `id_cliente` opcional, `token`. | `body[]`: `CODIGO`, `NUMERO`, `CATEGORIA`, `VALOR`, `CODIGO_AREA` (L66–73). Exclui categorias `800` e `3003` (L56–57). |
| `listDids0800` | `GET /dids/0800_disponiveis` L84 | Query: `id_cliente` opcional. | mesmo shape; `categoria = '800'` (L112). |
| `getPlan` | `GET /plano_by_id` `rotas/BuscarPlanos.php` L9 | Query: `id_plano` (obrigatório L19). | JSON array: `id`, `id_plano_disponivel`, `id_categoria`, `categoria`, `tipo_numero`, `nome`, `descricao`, `cota_minutagem_movel`/`cota_minutagem`, `valor`, `valor_revenda`, `ativo`, `produtos[]`. |
| `listPlans` | `GET /planos_disponiveis` L80 | Query: `token`/`TOKEN`, `id_tipo_plano` opcional (L92). | mesmo shape; `visivel=1` e `ativo=1`. |
| `getCustomerMonthly` | `GET /get_mensalidade_cliente` L154 | Query: `id_cliente`. | soma extras (`Planos::SomarValoresExtraByCliente`). |
| `listCharges` | `POST /financeiro/adm/cobrancas_cliente` `rotas/Financeiro/financeiro_adm.php` L67 | Body: `id_cliente`, `token`. Verifica projeto (`VerifyDataProject::verifyClientProjeto`). | cobranças do cliente no projeto. |
| `listCustomerCharges` | `POST /financeiro/cliente/cobrancas` `financeiro_cliente.php` L12 | Body deveria ter `id_cliente` (L16); **bug legado**: L23 hardcoda `100009271`. **Não usar** no adapter até PR-LEG-04. | `id_assinatura`, `gateway`, `cobrancas`. |
| `createOrgStub` | `POST /criar-cliente` `rotas/WhiteLabelRoutes.php` L60 | JSON `{ email }` + projeto do middleware. | cria cliente pelo e-mail. Insuficiente para tronco. |
| `assignDid` / `removeDid` | `POST /setar-telefone-para-cliente`, `/remover-telefone-para-cliente` `WhiteLabelRoutes.php` L8–19 | `{ idPhone, customerId }` | — |
| `disableCustomer` | `POST /customers/{id_cliente}/disable` `ManageCustomerRoute.php` L10 | — | — |

**`dados_cliente`** persistido em `organizacoes` (`controllers/Clientes.php` L119–161): `tipo_pessoa`, `cpf_cnpj`, `razao_social`, `nome_fantasia`, `email`, `telefone`, `inscricao_estadual` (default `ISENTO`), `inscricao_municipal`, `endereco.{cep,logradouro,numero,bairro,complemento,cidade,estado}`, `administrativo.*`, `financeiro.*`, `legal.*`, `data_nascimento`, `empresa_data_fundacao`, `tipo_pagamento`, `vencimento`, `ddd_escolhido` (usado no tronco, `CadastroTronco.php` L32). Flag `cliente_projeto = 2` (L160) = white-label.

**`planos[]`** (`Cadastro.php` L408–436 + `PlanosClienteTronco.php` L38–70): `{ id, tipo, qtd_canais?, dids: [{ id, portabilidade_dados? }] }`. Insert em `projetos_tronco_assinaturas_planos` (`id_cliente`, `id_projeto_cliente`, `id_tipo_plano`, `id_plano`, `valor_revenda`, `valor_faturamento`, `qtd_canais`).

**`projetos_clientes_tronco`** (`CadastroTronco.php` L16, `sql/fpn-592_create_table_projetos_clientes_tronco.sql`): `id_cliente`, `instalado`, `url_contrato`, `ddd_escolhido`, `id_fidelidade`, `id_vencimento`, `id_projeto_cliente`, `id_estoque_dids`.

### 1.2 Criação de **projeto** (revenda)

Não há rota Slim para insert em `projetos_clientes`. O cadastro é o form WP `wp-content/themes/easy/modulo_white_label/cadastrar_projeto_whitelabel.php` L157–188: `nome`, `url`, `url_painel_pabx`, `token_cadastro_wl`, `domain_dns`, `dia_vencimento`, `have_app`, `have_tronco`, `token_adm`, `token_painel_pabx`, opcional SIP (`domain_sippulse`, `user_api_sip`, `pass_api_sip`, profiles 150/250/350/750/850/950, `cota_minutos_canal_750`). Schema base: `sql/fpn-592_create_table_projetos_clientes.sql` (`id`, `nome`, `url`, `token`, `have_tronco`, `domain_sippulse`, …).

**MVP:** operador cria o projeto no ERP e cola `legacy_project_id`. **Alvo:** PR-LEG-02 (endpoint interno autenticado por token de serviço, não o token WL do tenant).

### 1.3 Ativação telco no ERP

`api/src/ProjetosClientes/AtivacaoTronco.php`: cron/script percorre `instalado=0`, para cada DID escolhe prefixo `750` (voz, `tipo_numero==1`) ou `850` (0800) (L127–138), username `{prefixo}{idCliente}`, `insertSubscriber` (L212–228) com `profile`, `domain` (`getDomainSippulse`), `ratePlanId`, `callLimit` = `qtdCanais`, `activeOutgoingCalls`, senha `substr(md5($username),0,25)`; `insertDid` (L287–296); `updateCotaMensalProjetoCliente` só no 750 (L247–250); `updateSubscriberServices` com `callLimit` (L254–270); marca `instalado=1` (L311–336). Sem Fusion.

O commerce **não** dispara esse PHP por HTTP (não há rota). Opções: TelcoPort faz o SOAP direto (preferido — menos acoplamento) **e** espelha `instalado=1`; ou PR-LEG-03 expõe `POST /ativar_tronco`. `PADRÃO PROPOSTO`: SOAP direto via `TelcoPort`; ERP só cadastro (org/tronco/DIDs).

### 1.4 Idempotência, timeout, retry, erros

- Idempotência: `cadastro_total_tronco` é upsert por e-mail/cpf (`Cadastro.php` `VerificarCliente` L198–240). Adapter envia o mesmo payload; grava `legacy_org_id` no map. Não há header de idempotência no Slim.
- Timeout: `PADRÃO PROPOSTO` 30 s (cadastro pode ser pesado).
- Retry: só 5xx/timeout; 200 com `status=error` de negócio → não retry.
- LAB: allowlist `legacy_project_id` +, após create, o `id_cliente` retornado deve ser registrado na allowlist (senão o próximo write falha).

### 1.5 Reconciliação

Job `ReconciliationJob`: PG `legacy_id_map` vs `SELECT id, bloqueado, have_tronco FROM projetos_clientes` e `organizacoes.id_projeto_cliente` + `projetos_clientes_tronco.instalado`. Drift → `audit` + alerta ops. Sem write automático no ERP.

---

## 2. `TelcoPort` → `SipPulseSoapAdapter`

**Propósito.** Assinante SIP, DID, canais (`callLimit`), bloqueio. Sem rating no switch, sem `addCredit`, sem `ResellerWS`.

**Alvo.** SOAP SipPulse 4.2. WSDL `{IP_SIPPULSE}/{Endpoint}?wsdl` (`api/src/SipPulse/Client/BaseRequisition.php` L15–18). IP documentado: `http://177.11.49.222:8080`; VPN `172.16.49.222:8080` (brief + `AUDITORIA-SIPPULSE.md` L136). Constantes `IP_SIPPULSE`, `LOGIN_SIPPULSE`, `SENHA_SIPPULSE` no `wp-config` do ERP — o commerce usa vault `SIPPULSE_SOAP_URL`, `SIPPULSE_SOAP_LOGIN`, `SIPPULSE_SOAP_PASSWORD` (nomes only). Auth SOAP: `UserPrincipal{login,password}` no envelope (auditoria §3.1). As colunas `projetos_clientes.user_api_sip/pass_api_sip` **não** são usadas pelo factory (auditoria L141).

**Não usar:** `ResellerWS`, `RatePlanWS`, `addCredit` (auditoria §3.2 — ociosos).

### 2.1 Métodos SOAP (código)

Cliente: `api/src/SipPulse/Subscriber/Client/SubscriberClient.php`, `Dids/Client/DidClient.php`.

| Operação port | SOAP | Endpoint | Campos |
|---------------|------|----------|--------|
| `createSubscriber` | `insertSubscriber` L27–29 | `SubscriberWS` | `username`, `domain`, `password`, `profile`, `emailAddress`, `countryCode`, `areaCode`, `localArea`, `cityCode`, `callLimit`, `ratePlanId`, `contractNumber`, `document`, `firstName`, `lastName`, `activeOutgoingCalls`, `activeIncomingCalls`, `resellerMarkup=0` (`sip_pulse.php` L2705–2732; tronco L212–228). Username tronco: `750{orgId}` ou `850{orgId}`. |
| `updateServices` | `updateSubscriberServices` L72–74 | `SubscriberWS` | `username`, `domain`, `callLimit`, `cps`, `areaCode`, `localArea`, `countryCode`, `cityCode`, `keepAlive`, `topologyHiding` (AtivacaoTronco L254–269). **Não há** operação SOAP `callLimit` isolada — canais = campo `callLimit`. |
| `setMonthlyQuota` | `updateMonthlyQuota` L90–92 | `SubscriberWS` | `username`, `domain`, `monthlyQuota`, `monthlyQuotaType=M`, … Pré-pago: **`monthlyQuota` = saldo disponível da carteira** (atualizado a cada débito/top-up). **VERIFICAR** se a cota é monetária e enforced em tempo real; fallback D-BIZ-26: `blockOutgoingCalls` em ~90% do saldo (configurável). |
| `rotateSipCredentials` | `updateSubscriber` password (SOAP exato **VERIFICAR**) | `SubscriberWS` | Gera secret crypto-random; exibe **uma vez**; persiste hash no PG. |
| `block` | `blockSubscriber` L108–110 | `SubscriberWS` | `username`, `domain` (`sip_pulse.php` L1863–1870). Unblock = `activateSubscriber` L99–101 / L1876. Tronco bloqueia 750 **e** 850 (L1837, L1047). |
| `blockOrigination` | `blockOutgoingCalls` L126–128 | `SubscriberWS` | `username`, `domain`. `PADRÃO PROPOSTO` para pré-pago zero: cortar originação e manter destinação (`activateOutgoingCalls` L118). `A VERIFICAR` se o produto quer block total. |
| `insertDid` | `insertDid` DidClient L26–28 | `DidWS` | `username` (número), `accountCode`=`{sipUser}@{domain}`, `domain`, `aliasUsername`, `didToRuri`, `reverseBilling` (0800 true) (`sip_pulse.php` L2753–2765; AtivacaoTronco L287–296). 0800: prefixo `0` se começa com `800` (L284–286). |
| `deleteDid` | `deleteDid` L35–37 | `DidWS` | — |
| `listDids` | `listByAcc` L44–46 | `DidWS` | `accountCode` |
| `retrieve` | `retrieveSubscriber` L54–56 | `SubscriberWS` | reconciliação |

### 2.2 SLA adapter

- Timeout SOAP: `PADRÃO PROPOSTO` 20 s.
- Retry: só fault de transporte / HTTP 5xx do Apache; SOAP business fault → `legacy_rejected`.
- Idempotência: `insertSubscriber` em username existente falha — adapter faz retrieve e update. Map `legacy_id_map` chave `(sip, username, domain)`.
- LAB: allowlist de `username` + `domain` + DID E.164.
- Reconciliação: `retrieveSubscriber` + `listByAcc` vs `telco.sip_accounts` / `telco.dids`.

---

## 3. `CdrPort` → `SipPulseReportsAdapter`

**Propósito.** Leitura do CDR tarifado para o rating 30/6 no PG. **Não escreve** no switch.

**Alvo.** MySQL `sippulse_reports.cdrs_report`, usuário **somente SELECT** (`AUDITORIA-SIPPULSE.md` L137; `DBSipPulse.php` L12 — o ERP usa `DB_USER_SIPPULSE` / `DB_HOST_SIPPULSE` = `172.16.49.222`). Vault: `SIPPULSE_REPORTS_DSN` (user read-only). Host VPN.

**Não existe coluna `billsec` no código lido.** Duração usada pelo legado: `duration_block` (`sip_pulse.php` L832 `sum(cr.duration_block)`, L1117 export CSV `duration_block as DURACAO`). `DBSipPulse.php` L22 também seleciona `sum(duration)` — segunda coluna, `A VERIFICAR` se é alias ou campo distinto. Origem/destino no export: `src_ip`, `callee_id` (L1113–1116) — **não** `src`/`dst`. Assinante: `accountcode` no formato `750{id}@{domain}` (comentário `DBSipPulse.php` L16). Início: `call_start_time`. PK: `id`.

### 3.1 SELECT do import (`PADRÃO PROPOSTO`)

Campos a projetar (todos observados no código):

`id`, `call_start_time`, `accountcode`, `duration_block`, `duration` (`A VERIFICAR`), `callee_id`, `src_ip`, `user_agent`, `service_type`, `service`, `rate`, `price`.

Filtro: `id > :watermark AND call_start_time >= :overlap_start` (overlap 2 min para clock skew). Order `id ASC`. Limit 2000.

Map rating: `accountcode` → `telco.sip_accounts` → `subscription_id`. Destino: classificar `callee_id` em `landline_br` / `mobile_br` / `other`. **`raw_seconds` = `duration`** (segundos atendidos); cadência 30/6 **só no PG**. Persistir `duration_block` como auditoria — **nunca** tarifar em cima de `duration_block` (D-INT-01; gate E3: amostra CDR conta demo). `price` do switch = auditoria, não venda.

### 3.2 SLA

- Timeout query: 30 s.
- Sem retry de write (é read). Falha → próximo tick.
- Reconciliação: soma `duration_block` do dia vs soma `rated_calls` + orphans (accountcode sem map).
- LAB: SELECT pode ser amplo (read-only); se filtrar, allowlist de `accountcode`.

Volume: ~52 k/dia — ver `03-ARCHITECTURE.md` §12.

---

## 4. mePay — cobrança (`PaymentPort`) e fiscal (`FiscalPort`)

**Decisão operador 2026-11-04 (ADR-013).** Toda cobrança (cliente final **e** revenda — D-RM-16) e toda nota fiscal (NFCom modelo 62) passam **somente** pelo mePay. Pagamento **nunca** via WHMCS. VC1 não integra SyncNFe, Omie nem emissor NFCom diretamente; o mePay chama o emissor terceiro (NFCom ainda **a criar no mePay**; hoje mePay expõe NFS-e via Omie/ErpGateway).

**Contrato de referência (read-only):** `suites/mework360/me360-mepay/docs/contracts/openapi.yaml`, `events.md`, `ARCHITECTURE.md` (ErpGateway/Omie para NFS-e).

**Auth outbound.** Bearer `mpk_...` (company API key). Vault: `MEPAY_API_KEY`, `MEPAY_WEBHOOK_SECRET`. Toda `POST` mePay exige header `Idempotency-Key` (8–128 chars); replay devolve resposta armazenada; corpo diferente → 422.

**Payouts revenda:** transferência manual no Filament até mePay oferecer payout automático (`PADRÃO PROPOSTO`).

### 4.1 Mapeamento VC1 → mePay (PaymentPort)

| Operação VC1 | mePay (openapi / evento) | Notas |
|--------------|--------------------------|-------|
| `upsertCustomer` | `POST /v1/customers`, `PATCH /v1/customers/{id}` | `externalRef` = `tenant:{reseller_slug}` ou UUID commerce |
| `createInvoice` (fechamento) | `POST /v1/invoices/adhoc` | Linhas `InvoiceLineInput`; aprovação > R$ 5k via `/approve` |
| `createPaymentLink` | `POST /v1/invoices/{id}/charges` | Pix/boleto/cartão (cartão exige vault token); persiste `payment_link_url` |
| `confirmPayment` (fallback) | `POST /v1/invoices/{id}/confirm` | Polling/callback gateway |
| `getInvoice` | `GET /v1/invoices/{id}` | Reconciliação diária |
| `hostedCheckout` (funil E5) | `POST /v1/checkouts` + `POST .../complete` | Sessão 24h |
| Assinatura SaaS (fora escopo voz) | `POST /v1/subscriptions` | Não usado no MVP tronco |
| LAB simular pagamento | `POST /v1/lab/payments/simulate` | Só `MEPAY_LAB=1` |

Não há split de pagamento (brief).

### 4.2 Mapeamento VC1 → mePay (FiscalPort)

| Operação VC1 | mePay | Notas |
|--------------|-------|-------|
| `requestFiscalNote` | **a criar no mePay** — ex.: `POST /v1/invoices/{id}/fiscal-documents` ou `POST /v1/fiscal/nfcom` | Body: model `62`, issuer CNPJ, tomador, linhas espelhando fatura |
| `getFiscalNoteStatus` | **a criar no mePay** — ex.: `GET /v1/fiscal-documents/{id}` | Até webhook chegar |
| `downloadFiscalArtifacts` | **a criar no mePay** — URLs PDF/XML ou proxy | VC1 grava em `files.objects` |

Emitente legal: CNPJ RLGR quando VC1 fatura; CNPJ revenda quando aplicável — **sem** mudança de negócio, só caminho técnico.

### 4.3 Webhooks inbound (mePay → commerce)

`POST https://api.vc1.com.br/api/v1/webhooks/mepay` (D-INT-18, D-SEC-09).

Assinatura **Svix-compatible** (`events.md`): headers `webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64(HMAC-SHA256(secret, "{id}.{timestamp}.{body}"))>`. Rejeitar carimbo > 5 min. `webhook-id` = chave de idempotência **persistida** em `integration.inbox_webhooks`.

| Evento mePay (documentado) | Uso VC1 |
|----------------------------|---------|
| `invoice.paid` | Baixa `billing.payments`, `invoice.paid_at`, desbloqueio telco (§10.6) |
| `invoice.created` | Cache portal / notificação opcional |
| `invoice.overdue` | Alimenta dunning |
| `dunning.action` / `dunning.resolved` | Suspender/reativar (modo SaaS; avaliar paridade voz) |
| `subscription.*` | Fora do MVP tronco |
| `fiscal.issued` | **a criar no mePay** — `access_key`, refs PDF/XML → `fiscal_documents.status=issued` |
| `fiscal.failed` | **a criar no mePay** — `fiscal_documents.status=failed` + ops |

### 4.4 Idempotência e SLA

- Outbound: `Idempotency-Key` = `{use_case}:{aggregate_id}:{cycle_or_event}` (gravado também em `integration.connector_calls`).
- Timeout adapter: 15 s (charges); fiscal async pode ser 202 + webhook.
- Retry: 5xx exponential; 4xx business → não retry cego.
- LAB: `FakePaymentPort` + `FakeFiscalPort` (E0–E4/E5 até NFCom mePay); contrato test em `test/contracts/` espelhando mePay.

### 4.5 Dependência cross-repo (bloqueador)

| Item | Owner | Repo |
|------|-------|------|
| mePay expor NFCom modelo 62 via emissor terceiro (API + webhooks `fiscal.*`) | **mePay** | `me360-mepay` |

Até lá: LAB usa `FakeFiscalPort`; rastrear pedidos fiscais no mePay quando API existir.

---

## 5. SyncNFe / RLGR — histórico (supersedido por ADR-013, 2026-11-04)

> **Supersedido por ADR-013 (2026-11-04).** Caminho **supersedido**. VC1 **não** implementa `SyncNfeNfcomAdapter`. Referência abaixo preservada para migração do legado e entendimento do pipe-delimited ImportNFModelo62. Implementação atual: **FiscalPort → mePay** (§4).

**Propósito (legado).** Emitir NFCom modelo 62 pelo emissor **RLGR** para `billing.invoices`: (E3 go-live) **`party=reseller`**; (E5+) **`party=customer`** no modo `vc1_invoices`. Só marcar autorizado após `ConsultaNFModelo62` + chave 44 dígitos.

**Alvo.** `https://api.syncnfe.com.br`. Auth: `POST api/Token` body `{"Login","Password"}` (`faturamento.php` L9054–9064). Credencial **não** está no código: parâmetros `intelbras_controle_fiscal.sync_nfe_login/sync_nfe_pass` (AGENT-NOTES falepaco-suite 2026-10-01). Vault: `SYNCNFE_LOGIN`, `SYNCNFE_PASSWORD`.

### 5.1 Import (legado)

`POST api/ImportNFModelo62` L9082–9087:

```json
{
  "Token": "<token numérico da api/Token>",
  "ArquivoMestre": ["<pipe-delimited tomador>"],
  "ArquivoDetalhes": ["<pipe-delimited serviço>"],
  "LiberarEnvioAoImportar": true
}
```

**ArquivoMestre** (`stringClienteCampos` L8972–9010), campos na ordem: id_cliente, id_cliente, razão, endereço, número, complemento, bairro, cidade, UF, CEP, CNPJ/CPF, IE/RG, (vazio 18), (vazio 17), dia vencimento, modelo `"62"`, CFOP (`5307` intra-SP / `6307` inter, L8654–8660), telefone, e-mail, id_cliente, `"3"`, `"4"`, data emissão, data competência, `"1"`, observação `"Dids: …"`, IBGE município 7, `"3"`, `"0"`, início contrato, (vazio), IE de novo, competência `Ym`, `"0"`, data vencimento completa, (vazio), (vazio), `dataFimPeriodo`.

**ArquivoDetalhes** (L9021–9049): id_cliente, identificador (id cobrança), descrição `'Servico de Terminacao de Trafego de Voz (STTV)'`, valor, `"0,00"`, `"0"`, `"UN"`, `"1"`, `"1"`, `"0,00"`, código `"0100101"`, valor, zeros, CST `"90"` / `"49"`, `"true"` no último campo.

Resposta sucesso legado: `Sucess == 1` (sic) L9127; `Notas[0].NotaFiscalID`, `NumeroNF`, `Url`, `GUID`, `Total`. O legado grava `Chave` = **GUID** (L9138), **não** a chave SEFAZ 44 dígitos.

TLS verify está **off** no legado (L9083) — adapter liga.

### 5.2 Consulta (o legado **não** chama)

QA-009 (`easy_cicerone/docs/FINDINGS.md` L96–105, L358+): nenhum arquivo consulta status após o import. ~1 600 notas em “Erro no envio”. AGENT-NOTES (falepaco-suite L20): `POST api/ConsultaNFModelo62` com `Token`, `DataInicio`/`DataFim` `yyyy-mm-dd`, faixa `NumeroNF` ou `NotaFiscalID` (até 1 000), `GUID` opcional. Devolve `Notas[].Status` (`"NFe enviada com sucesso"` / `"Ocorreu um erro no envio da NFe"`), `Erros` (ex. `"430 : Rejeição: IE do Destinatário não informada"`), `Chave` **44 dígitos só quando autorizada**. `api/ConsultaNF62` é **outra rota** (postback NF recebida) — não usar.

DBA-003: legado insere `notas_lotes` **antes** da API (L8650) sem transação. O commerce grava `fiscal.fiscal_attempts` primeiro; `fiscal_documents.status=imported` só com HTTP ok; `authorized` só se `ConsultaNFModelo62` + `Chave` length 44.

### 5.3 SLA

- Timeout Token+Import: 45 s (legado ainda dá `sleep(5)` em IE — QA sleep; adapter **não** dorme).
- Poll: `FiscalStatusPollJob` 15 min (D-ARQ / D-INT-02).
- Retry import: 5xx; rejeição SEFAZ → `rejected` + ops.
- Idempotência: 1 documento por `invoice_id`. Reemitir só se `rejected`/`cancelled`.
- LAB: CNPJ demo na allowlist SyncNFe (`A VERIFICAR` ambiente homolog).

---

## 6. `SignaturePort` → `MeSignAdapter`

**Propósito.** Contrato da proposta (Produto 02) e, se aplicável, contrato da revenda com a VC1.

**Alvo.** `https://mesign.mework360.com.br` (brief). Código: `sales-mework360/backend/app/Services/Signature/PacoAssignClient.php`. Auth: Bearer `config('signature.paco.api_key')` — vault `MESIGN_API_KEY`. Timeout doador: `signature.paco.timeout` default **60 s** (L18).

Rotas meSign (`mesign/routes/api.php` L8–24), middleware `auth.apikey` + `org.scope` + throttle 120/min:

| Operação | HTTP | Campos (StoreEnvelopeRequest L49–82 + PacoSignatureFlow L115–140) |
|----------|------|---------------------------------------------------------------------|
| `createEnvelope` | `POST /api/v1/envelopes` | `title`, `external_id` (sale UUID), `sender.{name,email}`, `document.{name, source=html\|pdf, content_base64}`, `signers[]` (`name`, `email`, `phone_e164`, `auth_method=sms_otp\|link_accept`, `send_invite`), `webhook_url`, `webhook_secret`, `metadata`, opcionais `fields[]`, `expires_at`, `confirmation_factors` |
| `getEnvelope` | `GET /api/v1/envelopes/{id}` | — |
| `download` | `GET /api/v1/envelopes/{id}/download` | `type=original\|final\|certificate` |
| `resend` | `POST /api/v1/envelopes/{id}/resend` | sem body obrigatório |
| `cancel` | `POST /api/v1/envelopes/{id}/cancel` | `{ reason? }` |

Webhook **outbound** meSign → commerce: `POST {webhook_url}` com `X-Paco-Signature: sha256={hmac}` (`PacoAssignWebhookController.php` L170) e `X-Paco-Event`. Eventos: `envelope.completed` (SendWebhookJob) e `envelope.archived` (`archive_url`, `archive_path`, sem bytes). Path doador: `POST /api/v1/webhooks/paco-assign` (`routes/api.php` L39). Commerce: `https://api.vc1.com.br/api/v1/webhooks/paco-assign`. Secret: `MESIGN_WEBHOOK_SECRET` (o doador manda `webhook_secret` **no create** — L132).

Após `completed`: persistir sha256, marcar `pending_signature` → `contracted`, disparar TelcoPort (não `StartPlatformOnboardingJob`). Após `archived`: guardar PDF no object storage.

Idempotência: `external_id` = `sales.contracts.id` / sale UUID. Retry 5xx. LAB: org meSign de demo na allowlist; SMS driver log.

---

## 7. `DnsPort` → `MeDnsAdapter`

**Propósito.** Zona `vc1.com.br` no meDNS (PowerDNS + MariaDB Galera, ns1–ns4 Eveo+BRS). Wildcard cobre `{slug}`. Vanity domains depois. ACME DNS-01 para TLS `*.vc1.com.br`.

**Alvo.** PowerDNS REST `/api/v1/servers/localhost/zones` (`mecloud-meDNS/docs/OPERATIONS.md` L714, L826). Auth `X-API-Key`. VMs commerce na LAN meCloud: **`dns-master:8443`** com **API key escopada por Account** (D-INF-25, ADR-009; ADR-013 proíbe expor a API PDNS na Internet e proíbe key Administrator — VPN/LAN ok). Vault: `MEDNS_API_URL`, `MEDNS_API_KEY` (escopo mínimo da zona `vc1.com.br`) — **nunca** key global PDNS no contexto do agente.

ACME DNS-01 já feito na suíte via `PATCH REPLACE` na API PDNS de `_acme-challenge` (OPERATIONS.md L7–11, L17–24). Adapter:

| Operação | Transporte | Notas |
|----------|------------|-------|
| `presentAcmeChallenge` | PATCH rrset TXT `_acme-challenge.vc1.com.br` TTL 60 | valor do token ACME; notify SOA |
| `cleanupAcmeChallenge` | DELETE / PATCH vazio | após emissão |
| `ensureWildcard` | CNAME/A `*.vc1.com.br` | uma vez no bootstrap (operador) |
| `addVanity` (E1+) | CNAME → edge | futuro |

Timeout PDNS client meDNS: 30 s (`pdns_client.py` citado no F35 brief). Retry só GET 502/503; mutação **uma** tentativa (mesmo brief). LAB: zona `lab.vc1.com.br`. Reconciliação: GET zone vs `tenancy.reseller_domains`.

---

## 8. `NotificationPort` → `SmtpMailAdapter` + `SmsAdapter`

**Propósito.** E-mail transacional (proposta, 80/100, fatura, payout) e SMS (PIN opcional, OTP meSign já é do meSign).

**Auth.** `MAIL_*` Laravel / `SMS_DRIVER` (doador meSign usa Disparos na platform). Vault: `SMTP_URL`, `SMS_API_KEY`. `PADRÃO PROPOSTO`: SMTP meCloud; SMS = mesmo Disparos do meWork se o operador autorizar, senão log em LAB.

Timeout 10 s. Sem PII demais nos logs. LAB: driver `log`.

---

## 9. Guard LAB e observabilidade

`LabGuardedProvisioningAdapter` etc. envolvem o adapter real:

1. Resolve `legacy_id` do request.
2. `SELECT 1 FROM integration.lab_allowlist WHERE kind=? AND legacy_id=?`.
3. Miss → `lab_write_refused` + métrica; **zero** HTTP/SOAP.
4. Hit → delega e ainda grava `connector_calls`.

Allowlist inicial (operador): 1 `projetos_clientes` demo, N `organizacoes`, usernames `750*`/`850*` desse org, DIDs de estoque de teste. Criar contas demo em **produção** exige aprovação explícita (brief).

`integration.connector_calls`: `id`, `port`, `operation`, `idempotency_key`, `request_hash`, `status`, `http_or_soap_code`, `duration_ms`, `legacy_id`, `error_code`, `created_at`. Retention `PADRÃO PROPOSTO` 90 dias.

---

## 10. Mudanças no legado (PRs pequenos, aprovação do operador)

Nenhum destes PRs é feito por este blueprint. Cada um = branch `front/*` no repo legado + PR. Sem push em main.

### PR-LEG-01 — inventário de jobs de fatura legado + skip VC1 (obrigatório antes do piloto)

**Problema.** Vários crons WL/tronco ainda geram títulos no ERP (`geracao_faturas_whitelabel_tronco.php`, `criarFaturamentoClienteWl.php`, minutagem e irmãos) → **cobrança dupla** se o projeto piloto não for filtrado.

**Entrega PR-LEG-01 (gate 1º PROD):** inventário completo de **todos** os jobs de billing WL/tronco; em **cada** path, o mesmo predicado `projetos_clientes.skip_tronco_billing = 0`; **golden test** por path (zero título novo no ciclo piloto).

**Flag mínima (`PADRÃO PROPOSTO` D-INT-03):**

```sql
ALTER TABLE projetos_clientes
  ADD COLUMN skip_tronco_billing TINYINT(1) NOT NULL DEFAULT 0;
```

No SELECT de projetos (L77–83), acrescentar `AND pc.skip_tronco_billing = 0`. Default 0 = comportamento atual. Onboarding VC1 grava `1`.

Não reusar `have_tronco` (vale 1 também para WL tronco não-VC1).

### PR-LEG-02 — criar `projetos_clientes` via API (onboarding)

Endpoint interno `POST /api/white_label/projetos` (ou `api/public` com token de serviço, **não** o token WL do tenant) com o subset de `cadastrar_projeto_whitelabel.php`: `nome`, `have_tronco=1`, `domain_sippulse`, profiles 750/850, `skip_tronco_billing=1`, `url_painel_pabx` placeholder. Sem isso o Filament só cola `legacy_project_id`.

### PR-LEG-03 — opcional: `POST /ativar_tronco`

Só se o operador recusar SOAP direto do commerce. Expõe `AtivacaoTronco::executar` filtrado por `id_cliente`. Preferência da arquitetura: **não** fazer; TelcoPort fala SOAP.

### PR-LEG-04 — bug `financeiro/cliente/cobrancas`

`financeiro_cliente.php` L23 hardcoda `idCliente = 100009271`. Corrigir para `$dados['id_cliente']`. O commerce não deve usar essa rota até o fix; usa `financeiro/adm/cobrancas_cliente`.

### PR-LEG-05 — crons irmãos (dentro do inventário PR-LEG-01)

Mesmo predicado `skip_tronco_billing=0` em cada cron listado no inventário (ex.: `criarFaturamentoClienteWl.php`, wrappers de minutagem). Um PR por cron ou um PR agregado conforme operador.

### PR-LEG-06 — não copiar dívidas

Fora de escopo VC1. NFCom no commerce segue mePay (ADR-013); legado SyncNFe permanece no ERP até desligamento.

---

## 11. Registro na suíte `falepaco`

Schema: `/home/beesy/.cursor/skills/cursorsession-schema/references/cursorsuite.schema.json` (`repos[]` exige `id`, `role`, `path` relativo — **não** absoluto; `contracts[]` exige `id`, `provider`, `consumers`, opcionais `spec`, `test_cmd`). Manifesto atual: `/home/beesy/Projetos/Cursor/suites/falepaco/falepaco-suite/.cursorsuite` (`contracts: []` hoje, comentário “não inventar”).

**Proposta** (aplicar em sprint de âncora, não neste repo):

```yaml
  - id: commerce-vc1
    role: member
    path: ../commerce-vc1
    remote: https://github.com/SoftwareBeesy/commerce-vc1.git
    test_cmd: "cd backend && ./vendor/bin/pest --parallel"

contracts:
  - id: provisioning.port.v1
    provider: easy_cicerone
    consumers: [commerce-vc1]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
  - id: telco.sippulse.soap.v1
    provider: falepaco-sippulse-old
    consumers: [commerce-vc1, easy_cicerone]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
  - id: cdr.sippulse_reports.v1
    provider: falepaco-sippulse-old
    consumers: [commerce-vc1]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
  - id: signature.mesign.v1
    provider: mesign
    consumers: [commerce-vc1]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
  - id: fiscal.syncnfe.nfcom62.v1
    provider: commerce-vc1
    consumers: [commerce-vc1]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
  - id: dns.medns.v1
    provider: mecloud-meDNS
    consumers: [commerce-vc1]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
  - id: payment.mepay.v1
    provider: mepay
    consumers: [commerce-vc1]
    spec: ../commerce-vc1/docs/blueprint/06-INTEGRATIONS.md
```

`mepay` / `mesign` / `mecloud-meDNS` podem ser `complementary` se o âncora falepaco não os listar como members (mesign vive na suíte mework360). `PADRÃO PROPOSTO`: consumers apontam o id mesmo que o repo more noutra suíte; âncora não precisa clonar meSign. Path `../commerce-vc1` assume o clone ao lado de `falepaco-suite` (hoje o workspace está em `suites/falepaco/commerce-vc1` — bate).

`deploy_order` (`PADRÃO PROPOSTO`): `easy_cicerone` (flag skip) → `commerce-vc1` → DNS wildcard.

---

## 12. Caminho de troca E6 — `falepaco-api` / `falepaco-erp` / Kamailio

Hoje `falepaco-api` é fase 0 (`docs/SETUP-DECISIONS.md`, `docs/EVENT-CATALOG.md`). Hub = gateway + barramento; **ERP permanece SoT de domínio** (Decision #2). Eventos já catalogados que o commerce passará a **consumir** em vez de SOAP/Slim:

| Evento hub | Troca |
|------------|--------|
| `customer.created` / `customer.blocked` / `customer.unblocked` | no lugar de `cadastro_total_tronco` + `alterar_status_boqueio` |
| `charge.paid` | no lugar do webhook mePay direto (hub normaliza) |
| `case.advanced` / `numbers.portability.holder_changed` | funil + portabilidade |
| jobs `provision_device` | Telco Kamailio / dSIPRouter |

Swap **sem mudar controllers**:

| Port | Adapter MVP | Adapter E6 |
|------|-------------|------------|
| `ProvisioningPort` | `LegacyErpProvisioningAdapter` | `FalepacoApiProvisioningAdapter` (`apifalepaco.beesy.ia.br`, Decision #7) → ERP `tenancy`/`customers`/`numbers` |
| `TelcoPort` | `SipPulseSoapAdapter` | `KamailioTelcoAdapter` (falepaco-telco / dSIPRouter) |
| `CdrPort` | `SipPulseReportsAdapter` | `KamailioCdrAdapter` |
| `PaymentPort` | `MePayBillingAdapter` | hub pode normalizar `invoice.paid` |
| `FiscalPort` | `MePayFiscalAdapter` | hub **não** fala com emissor direto |
| `SignaturePort` | `MeSignAdapter` | permanece |

Critério para ligar E6 (plano): lab ERP com portal parceiro **em código** (hoje só `docs/ux/partners.md`) **e** classe 4 nova em produção. Até lá, ports estáveis, um bind no `AppServiceProvider`. Dual-run: `legacy_id_map` ganha `erp_uuid` sem apagar `legacy_org_id`.

Host hub: `apifalepaco.beesy.ia.br`. Auth M2M: tokens de serviço com scopes (Decision #4) — `PADRÃO PROPOSTO` quando o OpenAPI v1 existir.

---

## Itens para 10-DECISIONS

| ID | Item | Default |
|----|------|---------|
| D-INT-01 | Campo de duração CDR 30/6 | Tarifar `duration`; guardar `duration_block`; validar amostra demo **antes E3** |
| D-INT-07 | Cota SipPulse pré-pago | `updateMonthlyQuota` = saldo carteira; **VERIFICAR** enforcement; fallback block 90% |
| D-INT-02 | Intervalo `ConsultaNFModelo62` | `PADRÃO PROPOSTO`: 15 min; authorized só com Chave 44 dígitos |
| D-INT-03 | Nome da flag no ERP para pular cron tronco | `PADRÃO PROPOSTO`: `projetos_clientes.skip_tronco_billing` TINYINT default 0 |
| D-INT-04 | Onboarding cria projeto via API nova vs colar id | `PADRÃO PROPOSTO`: colar id no MVP; PR-LEG-02 em seguida |
| D-INT-05 | Ativação SOAP direto vs PR-LEG-03 | `PADRÃO PROPOSTO`: TelcoPort SOAP direto |
| D-INT-06 | Pré-pago zero: quota vs block | `updateMonthlyQuota` + fallback `blockOutgoingCalls` (D-BIZ-26) |
| D-INT-08 | Contrato mePay (rotas, HMAC, refunds) | `A VERIFICAR com o time do mePay`; mínimo §4 |
| D-INT-09 | Payouts via mePay vs transferência manual | `PADRÃO PROPOSTO` do brief: manual até o produto oferecer |
| D-INT-10 | ACME via medns-proxy operator vs PATCH PDNS direto | LAN `dns-master:8443` + key escopada por Account (igual D-INF-25); não medns-proxy SaaS nem key global |
| D-INT-11 | `ratePlanId` SipPulse dos subscribers VC1 | `A VERIFICAR` com ops (plano “sem tarifa comercial” / zero) |
| D-INT-12 | Path base `api/white_label` em PROD | `A VERIFICAR` (`easy-adapt` vs clone); env `LEGACY_WL_BASE_URL` |
| D-INT-13 | Timeout SOAP / cadastro tronco | `PADRÃO PROPOSTO`: 20 s SOAP, 30 s cadastro |
| D-INT-14 | LAB allowlist inicial (ids) | `A VERIFICAR` com operador; criar demo exige aprovação |
| D-INT-15 | SMS NotificationPort | `PADRÃO PROPOSTO`: Disparos se autorizado; senão log |
| D-INT-16 | Contratos no `.cursorsuite` da âncora | `PADRÃO PROPOSTO`: lista §11; `mepay` complementary |
| D-INT-17 | Dual-run E6 (`legacy_id` + `erp_uuid`) | `PADRÃO PROPOSTO`: colunas convivem até cutover |
| D-INT-18 | Webhook mePay path e HMAC | `PADRÃO PROPOSTO`: `/api/v1/webhooks/mepay` + HMAC-SHA256 `sha256=` |
| D-INT-19 | Token WL em GET (`generate_login_link`) | aceitar query só nessa rota; documentar dívida R2 isolada no adapter |
| D-INT-20 | Homolog SyncNFe para LAB | **Supersedido** ADR-013 — N/A (fiscal via mePay) |
