# 04 — Banco de dados (commerce-vc1)

> Blueprint de dados. Nomes, enums e papéis: `_BRIEF.md`. DDL canônico: `schema.dbml`.
> PostgreSQL 16 · um database `commerce_vc1` · schemas = bounded contexts.
> Dinheiro em `*_cents bigint`. Tarifas por minuto: `*_cents_per_min numeric(12,4)` (**PADRÃO PROPOSTO**, D-DB-01).

---

## 1. Convenções

Herdadas de `falepaco-erp/docs/DATABASE.md` e `docs/db-schema.dbml`, adaptadas ao fork multi-tenant:

| Convenção | Regra |
|-----------|--------|
| PK | `uuid` DEFAULT `gen_random_uuid()` |
| Tempo | `timestamptz`; `created_at` / `updated_at` nas entidades mutáveis |
| Soft delete | `deleted_at` em entidades de negócio (revenda, cliente, plano, proposta, fatura, DID…) |
| Append-only | sem `updated_at`/`deleted_at`: `proposal_events`, `rated_calls`, `credit_ledger`, `invoice_lines`, `block_events`, `channel_changes`, `limit_events`, `dunning_events`, `audit_log`, `fiscal_attempts` |
| Dinheiro | `*_cents bigint` (BRL × 100, inteiro). Nunca `double`/`numeric` para totais |
| Tarifa / min | `*_cents_per_min numeric(12,4)` — centavos de real por minuto, 4 casas (D-DB-01) |
| Cadência | `cadence_min_s=30`, `cadence_inc_s=6` na base de preços |
| Legado | `legacy_id` / `legacy_*` + `integration.legacy_id_map` |
| Qualificação | `schema.table` no Eloquent; **sem** `search_path` dinâmico |
| Tenant | toda tabela *tenant-owned* tem `reseller_id uuid NOT NULL` (ou NULL só onde o brief admite global). Índice líder `(reseller_id, …)` |
| FK | índice em toda FK. `ON DELETE Restrict` no agregado; `Cascade` em filhos de lifecycle (events, items); `SetNull` em ator/arquivo opcional |
| Unique vivo | índices parciais `WHERE deleted_at IS NULL` |
| Snapshots imutáveis | `price_base_versions` (após `published_at`), `plan_versions`, `sales.proposals.snapshot` (após sair de `draft`), `billing.invoice_lines`, `tenancy.reseller_contracts.snapshot`, `sales.contracts.snapshot` |
| Segredos | hash de token/PIN/senha; nunca senha SIP, token ERP, SOAP ou chaves mePay |
| Infra Laravel | `public.jobs`, `job_batches`, `failed_jobs`, `cache`, `sessions`, `password_reset_tokens` — **fora** dos schemas de negócio |

**Isolamento:** global scope Eloquent `where reseller_id = currentTenant()` é a barreira primária. RLS é **PADRÃO PROPOSTO** opcional (D-DB-04, §7).

**Tabelas globais (sem `reseller_id` obrigatório):** `identity.roles`, `identity.permissions`, `identity.role_permissions`, `pricing.price_bases`, `pricing.price_base_versions`, `pricing.vc1_tiers`, `pricing.price_base_version_rows`, `catalog.destinations`, `catalog.suggested_packages`, `rating.destination_prefixes`, `integration.lab_allowlist`, `integration.legacy_id_map`. `identity.users.reseller_id` é NULL para staff VC1.

---

## 2. Visão ER

### 2.1 Alto nível

```mermaid
erDiagram
  TENANCY ||--o{ PRICING_CLONE : "faixas da revenda"
  TENANCY ||--o{ CATALOG : planos
  TENANCY ||--o{ SALES : funil
  TENANCY ||--o{ CUSTOMERS : clientes
  CUSTOMERS ||--o{ TELCO : tronco
  CUSTOMERS ||--o{ RATING : uso
  CUSTOMERS ||--o{ BILLING : fatura
  BILLING ||--o{ FISCAL : NFCom
  TENANCY ||--o{ INTEGRATION : ports
  IDENTITY ||--o{ TENANCY : logins
  PRICING ||--o{ CATALOG : "tier travado"
  CATALOG ||--o{ SALES : proposta
  SALES ||--o{ CUSTOMERS : contrato
```

### 2.2 `identity`

```mermaid
erDiagram
  identity_users ||--o{ identity_user_roles : has
  identity_roles ||--o{ identity_user_roles : grants
  identity_roles ||--o{ identity_role_permissions : has
  identity_permissions ||--o{ identity_role_permissions : used
  identity_users ||--o{ identity_api_tokens : issues
  tenancy_resellers ||--o{ identity_users : "staff revenda"
  customers_customers ||--o{ identity_users : "login /app"
```

### 2.3 `tenancy`

```mermaid
erDiagram
  tenancy_resellers ||--o{ tenancy_reseller_domains : hosts
  tenancy_resellers ||--|| tenancy_reseller_branding : marca
  tenancy_resellers ||--o{ tenancy_stfc_licenses : outorga
  tenancy_resellers ||--|| tenancy_reseller_settings : defaults
  tenancy_resellers ||--o{ tenancy_reseller_contracts : contrato_VC1
  pricing_vc1_tiers ||--o{ tenancy_resellers : "faixa contratada"
```

### 2.4 `pricing`

```mermaid
erDiagram
  pricing_price_bases ||--o{ pricing_price_base_versions : versions
  pricing_price_bases ||--o{ pricing_vc1_tiers : bands
  pricing_price_base_versions ||--o{ pricing_price_base_version_rows : rates
  pricing_vc1_tiers ||--o{ pricing_price_base_version_rows : of
  pricing_price_base_versions ||--o{ pricing_reseller_tier_tables : cloned
  tenancy_resellers ||--o{ pricing_reseller_tier_tables : owns
  pricing_reseller_tier_tables ||--o{ pricing_reseller_tier_rows : rows
  pricing_vc1_tiers ||--o{ pricing_reseller_tier_rows : locked
```

### 2.5 `catalog`

```mermaid
erDiagram
  catalog_suggested_packages ||--o{ catalog_plans : cloned_as
  tenancy_resellers ||--o{ catalog_plans : owns
  catalog_plans ||--o{ catalog_plan_versions : versions
  catalog_plan_versions ||--|| catalog_plan_quotas : quotas
  catalog_plan_versions ||--|| catalog_plan_trial_rules : trial
  pricing_vc1_tiers ||--o{ catalog_plan_versions : locked_tier
  catalog_destinations ||--o{ rating_rated_calls : classifies
```

### 2.6 `sales`

```mermaid
erDiagram
  sales_proposals ||--o{ sales_proposal_items : lines
  sales_proposals ||--o{ sales_proposal_events : timeline
  sales_proposals ||--o| sales_trials : trial
  sales_proposals ||--o| sales_contracts : signed
  sales_contracts ||--o{ sales_signature_envelopes : meSign
  catalog_plan_versions ||--o{ sales_proposals : priced
  customers_customers ||--o{ sales_proposals : subject
```

### 2.7 `customers`

```mermaid
erDiagram
  tenancy_resellers ||--o{ customers_customers : owns
  customers_customers ||--o{ customers_addresses : addr
  customers_customers ||--o{ customers_contacts : roles
  customers_customers ||--o{ customers_customer_users : logins
  customers_customers ||--o{ customers_subscriptions : subs
  customers_customers ||--o{ customers_portability_requests : portout
  customers_portability_requests ||--o{ customers_portability_files : lgpd
  catalog_plan_versions ||--o{ customers_subscriptions : frozen
```

### 2.8 `telco`

```mermaid
erDiagram
  customers_subscriptions ||--o{ telco_sip_accounts : subscribers
  customers_subscriptions ||--o{ telco_dids : numbers
  customers_subscriptions ||--|| telco_channel_allocations : current
  customers_subscriptions ||--o{ telco_channel_changes : history
  customers_subscriptions ||--o{ telco_block_events : blocks
```

### 2.9 `rating`

```mermaid
erDiagram
  rating_cycles ||--o{ rating_usage_records : aggregates
  rating_cycles ||--o{ rating_rated_calls : atoms
  rating_cdr_imports ||--o{ rating_rated_calls : ingested
  telco_sip_accounts ||--o{ rating_rated_calls : accountcode
  rating_destination_prefixes ||--o{ rating_rated_calls : maps
```

### 2.10 `billing`

```mermaid
erDiagram
  billing_credit_wallets ||--o{ billing_credit_ledger : entries
  billing_credit_limits ||--o{ billing_limit_events : fires
  rating_cycles ||--o{ billing_invoices : billed
  billing_invoices ||--o{ billing_invoice_lines : freeze
  billing_invoices ||--o{ billing_payments : paid
  billing_invoices ||--o{ billing_dunning_events : chase
  rating_cycles ||--o{ billing_statements : reseller_invoices
  rating_cycles ||--o| billing_payouts : vc1_invoices
  billing_payouts ||--o{ billing_payout_lines : breakdown
  billing_payouts ||--o| billing_commission_invoices : nf_comissao
  billing_cycle_runs ||--o{ rating_cycles : orchestrates
  billing_payment_methods ||--o{ billing_payments : via
```

### 2.11 `fiscal` · `integration` · `audit` · `notifications` · `files`

```mermaid
erDiagram
  billing_invoices ||--o{ fiscal_fiscal_documents : nfcom
  fiscal_fiscal_documents ||--o{ fiscal_fiscal_attempts : mepay
  integration_outbox ||--o{ tenancy_resellers : publish
  integration_inbox_webhooks ||--o{ tenancy_resellers : receive
  integration_connector_calls ||--o{ tenancy_resellers : ports
  integration_reconciliation_runs ||--o{ rating_cycles : drift
  audit_audit_log ||--o{ tenancy_resellers : traces
  notifications_templates ||--o{ notifications_messages : render
  files_objects ||--o{ customers_portability_files : private
```

---

## 3. Tabelas (propósito, invariantes, regra de negócio)

Referência de regras pelo **nome da área** (IDs RN são escritos em paralelo).

### 3.1 `identity`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `users` | Login único (Filament, painel revenda, `/app`) | `actor_scope`, `reseller_id`, `customer_id` | Staff VC1: `reseller_id` NULL. E-mail único no tenant (parcial). Substitui `public.users` bigint |
| `roles` | Papéis do brief | `slug` unique | Somente os 10 slugs semeados |
| `user_roles` | N–N + tenant | `(user_id, role_id, reseller_id)` | Revenda não recebe papel `vc1_*` |
| `permissions` / `role_permissions` | ACL Filament/SPA | `slug` | Extra necessária ao RBAC |
| `api_tokens` | Sanctum / API pública | `token_hash`, `token_prefix` | Substitui `personal_access_tokens`. Segredo só no header |

### 3.2 `tenancy`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `resellers` | Tenant = revenda | `slug`, `billing_mode`, `commitment_cents`, `vc1_tier_id`, `legacy_project_id` | 1 linha ↔ `projetos_clientes`. Slug fora da lista reservada. `reseller_invoices` exige STFC verificado. Mínimo VC1 = uso próprio + custo dos clientes |
| `reseller_domains` | Resolução por `Host` | `host`, `is_primary` | `{slug}.vc1.com.br` primário; vanity depois (DnsPort) |
| `reseller_branding` | Marca em runtime | `logo_file_id`, cores | Sem SCSS compilado por tenant |
| `stfc_licenses` | Outorga Anatel | `license_number`, `verified_at` | Obrigatória para `reseller_invoices` |
| `reseller_settings` | Defaults comerciais | overage, 80/100 | Alerta margem negativa não bloqueia venda |
| `reseller_contracts` | Contrato VC1↔revenda | snapshot + faixa | Imutável após `signed_at` |

### 3.3 `pricing`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `price_bases` | Livro de preços VC1 | `cadence_min_s/inc_s` | Editor no painel VC1; 30/6 |
| `price_base_versions` | Versão com vigência | `effective_from`, `published_at` | Imutável depois de publicar. Nova vigência = novo insert |
| `vc1_tiers` | Identidade estável da faixa (`T1`…) | `code`, `price_base_id` | `locked_tier_id` aponta aqui — **sempre a faixa contratada**, sem upgrade automático pelo consumo |
| `price_base_version_rows` | Tarifas unitárias da versão | `min_commitment_cents`, `unit_*_cents`, `unit_*_cents_per_min` | Snapshot. DID, canal, min fixo, min móvel |
| `reseller_tier_tables` | Clone da versão VC1 | `markup_pct`, `is_current` | Uma tabela vigente por revenda |
| `reseller_tier_rows` | Edição linha a linha | `below_cost` | Faixas da revenda = faixas VC1 + % + override. Venda abaixo do custo: alerta, não bloqueio |

### 3.4 `catalog`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `destinations` | Enum persistido | `landline_br`, `mobile_br`, `other` | `other` bloqueado por padrão (D-DB-08) |
| `suggested_packages` | Pacotes sugeridos VC1 | `template` jsonb | Revenda clona e edita |
| `plans` | Cabeça do plano | `kind`, `reseller_id` | Pacote **ou** compromisso |
| `plan_versions` | Termos comerciais congelados | `sell_price_cents`, `commitment_cents`, `locked_tier_id`, `snapshot` | Imutável. Pacote: preço fechado + franquia. Compromisso: `max(consumo, compromisso)` na fatura |
| `plan_quotas` | DIDs, canais, franquia | `franchise_mode` `separate`\|`pooled` | Sobra de minutos **expira no mês** (não há saldo entre ciclos) |
| `plan_trial_rules` | Teste grátis do plano | `enabled`, `duration_days`, franquia | Definido pela revenda por plano |

### 3.5 `sales`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `proposals` | Funil `/p/{code}` + PIN 6 dígitos | `stage`, `kanban_column`, `snapshot` | Preço só no servidor. Snapshot imutável após `sent`. Kanban: `lead`/`negotiating`/`won`/`lost` derivado do `stage` |
| `proposal_items` | Linhas do snapshot | `unit_sell_cents`, `unit_cost_cents` | Congeladas no emit |
| `proposal_events` | Timeline | `event_type`, `actor_type` | Append-only (fork de `proposal_events`) |
| `trials` | Trial SIP | `status`, `ends_at` | Subscriber temporário via ProvisioningPort |
| `contracts` | Artefato assinado (substitui `public.sales`) | `issuer`, `snapshot` | Se VC1 fatura, contrato é da VC1 e o snapshot cita atendimento N1 + gestão comercial da revenda |
| `signature_envelopes` | meSign | `mesign_envelope_id` | Webhook HMAC `X-Paco-Signature` |

### 3.6 `customers`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `customers` | Cliente final ou uso próprio | `kind`, `legacy_org_id`, `legacy_tronco_id` | `kind=reseller_self` entra no mínimo da revenda. Documento único no tenant |
| `addresses` | NFCom / contrato | billing/main | Obrigatório para emitir NF |
| `contacts` | Papéis (legal, financeiro…) | `role` unique por cliente | Substitui `people` + `customer_people` |
| `customer_users` | Logins `/app` | `user_id` | Papéis `customer_admin` / `customer_user` |
| `subscriptions` | Assinatura viva | `plan_version_id` congelado, `status`, overage rates snapshotted | Status de bloqueio: crédito / limite / atraso / manual |
| `portability_requests` | Port-in | `legacy_portability_id` | Espelha `projetos_tronco_portabilidade` |
| `portability_files` | Anexos LGPD | `retained_until`, `file_id` | **Storage privado** — não copiar pasta pública do funil legado |

### 3.7 `telco`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `sip_accounts` | Espelho SipPulse | `username` 750/850/950, `accountcode`, `call_limit` | `rate_plan` legado = teto de rede, **não** fatura. Senha SIP não persiste |
| `dids` | Números ativos | `e164`, `activated_at` | Pró-rata pelos dias restantes no ciclo |
| `channel_allocations` | Canais atuais | `quantity` | Espelha `callLimit` |
| `channel_changes` | Histórico + pró-rata | `from/to_quantity`, `effective_on` | DID/canal no meio do mês = proporcional |
| `block_events` | Bloqueio originação | `reason` | Zero pré-pago, 100% pós, atraso, overage=`block`, manual |

### 3.8 `rating`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `cycles` | Competência | `period_start/end`, `status` | Mês calendário `America/Sao_Paulo` `[1º 00:00, 1º mês seguinte 00:00)` para todas as revendas (D-DB-13). `due_day` da revenda só vencimento |
| `destination_prefixes` | Classificação B-number | `prefix` → `destination` | Fixo BR preço único; móvel BR. Lista ANATEL **A VERIFICAR** |
| `cdr_imports` | Cursor de leitura | `cursor_after_id` | CdrPort read-only em `cdrs_report` |
| `usage_records` | Agregado ciclo×destino | `raw_seconds`, `billed_seconds`, `sell_cents`, `cost_cents` | Partição mensal. Tudo conta no mínimo: DID + canal + minutos |
| `rated_calls` | Átomo tarifado | `billed_seconds`, `sell_cents`, `cost_cents`, `legacy_cdr_id` | Partição mensal. Rating 2 níveis. **Não** reusar `price` do switch como venda |

### 3.9 `billing`

| Tabela | Propósito | Colunas-chave | Invariantes / regra |
|--------|-----------|---------------|---------------------|
| `credit_wallets` | Saldo pré-pago | `balance_cents`, `version` | `balance >= 0`. Recarga ≥ compromisso no plano mínimo |
| `credit_ledger` | Movimento | `amount_cents` numeric(14,4), `balance_after_cents`, `idempotency_key` | Débito do `sell_amount_cents` 30/6. Bloqueio ao zerar |
| `credit_limits` | Teto pós-pago | `limit_cents`, 80/100 | Exposição do ciclo |
| `limit_events` | Disparos 80/100 | `threshold_percent`, `action` | Alerta 80 (revenda+VC1), bloqueio 100 |
| `payment_methods` | mePay / manual | `kind`, `mepay_ref` | Repasse ainda é transferência manual (D-DB-10) |
| `invoices` | Fatura | `party`, `issuer`, `charged_cents`, `mepay_invoice_id`, `mepay_charge_id`, `payment_link_url` | Cobrança via mePay (revenda e cliente); `charged = max(subtotal, commitment)` |
| `invoice_lines` | Linhas imutáveis | `kind`, `snapshot` | Pró-rata, franquia, top-up de compromisso, shortfall |
| `statements` | Extrato tarifado | snapshot | Modo `reseller_invoices`: VC1 **não** emite NF ao cliente final |
| `payments` | Baixa | `paid_at`, `mepay_charge_id` | Só pago entra no repasse e (PADRÃO PROPOSTO) no mínimo VC1-fatura |
| `dunning_events` | Cobrança | `kind` | Não bloqueante por default de fatura aberta no mínimo |
| `cycle_runs` | Orquestração | `phase` | import → rate → aggregate → invoice → payout |
| `payouts` | Repasse | `paid_margin`, `shortfall`, `net_cents`, `status` | Só `vc1_invoices`. Sem split. Neto negativo → fatura VC1 à revenda |
| `payout_lines` | Decomposição | `kind` | Auditoria da fórmula §6 |
| `commission_invoices` | NF de comissão da revenda | `received_at` | Status `pending_commission_invoice` até receber |

### 3.10 `fiscal` · `integration` · `audit` · `notifications` · `files`

| Tabela | Propósito | Invariantes / regra |
|--------|-----------|---------------------|
| `fiscal_documents` | NFCom 62 via mePay | `requested` → `issued`/`failed`; `issued` só com chave 44 dígitos do webhook mePay; refs `mepay_invoice_id`, `mepay_fiscal_request_id`, `pdf_file_id`, `xml_file_id` |
| `fiscal_attempts` | Log chamadas mePay fiscal | Append-only; sem secrets |
| `outbox` | Eventos transacionais | Worker marca `published_at` |
| `inbox_webhooks` | meSign / mePay | Unique `(source, idempotency_key)` |
| `connector_calls` | Telemetria dos ports | Lab marca `lab_guarded` |
| `legacy_id_map` | UUID ↔ int legado | Unique `(entity_type, legacy_id)` |
| `reconciliation_runs` | PG vs SipPulse | billsec 30/6 vs `duration_block`/`price` |
| `lab_allowlist` | Allowlist de escrita LAB | Fora da lista = recusa |
| `audit_log` | Quem mudou preço/faixa/modo | Append-only |
| `notifications.*` | E-mail/SMS 80/100, bloqueio, fatura | Opt-out por destinatário |
| `files.objects` | Blob privado | Portabilidade com TTL; disco `private` |

---

## 4. Matemática de rating (30/6)

Fonte comercial: `_BRIEF.md` / PRODUCTS-vc1 §3. Motor **no PG**, a partir do CDR **bruto**, não do `price` do switch (PLAN §4 P14, risco R5).

### 4.1 Segundos faturáveis

Seja `raw_seconds` = `cdrs_report.duration` (segundos atendidos). `duration_block` persiste em `switch_duration_block` **somente** para reconciliação — **nunca** aplicar 30/6 em cima de `duration_block` (D-DB-15). Validar com amostra CDR de conta demo antes do gate E3.

```
se raw_seconds <= 0: billed_seconds = 0
senão se raw_seconds <= 30: billed_seconds = 30
senão: billed_seconds = 30 + 6 * ceil((raw_seconds - 30) / 6)
```

Equivalente: `billed_seconds = 6 * ceil(max(raw_seconds, 30) / 6)` para chamadas com `raw_seconds > 0` (porque 30 já é múltiplo de 6).

### 4.2 Valor da chamada

```
amount = billed_seconds / 60 * rate_cents_per_min
```

Dois níveis:

- **Custo VC1** (`cost_cents`): tarifa da **faixa contratada da revenda com a VC1** (`price_base_version_rows` da versão no contrato da revenda), não a faixa do cliente.
- **Venda** (`sell_cents`): tarifa do `plan_version` (pacote: excedente se `overage=charge`; compromisso: tarifa da `reseller_tier_row` travada).

### 4.3 Arredondamento — **PADRÃO PROPOSTO** (D-DB-02)

`rated_calls.sell_amount_cents` e `cost_amount_cents` = **`numeric(14,4)`** (centavos de real, **sem** half-up por CDR). Débitos de carteira pré-paga e saldo (`credit_wallets.balance_cents`, `credit_ledger.amount_cents`) também **`numeric(14,4)`**.

**Half-up para inteiro** só nas **linhas agregadas** de fatura/extrato: por **classe de destino × assinatura × ciclo** (e linhas de pró-rata DID/canal no fechamento).

**Por quê não arredondar por chamada:**

1. Soma exata de `numeric(14,4)` no rating; arredondamento único na agregação evita viés acumulado por CDR.
2. Pré-pago debita o mesmo átomo tarifado; wallet e `rated_calls` permanecem alinhados.
3. Snapshot da fatura guarda centavos inteiros **depois** da agregação.

Franquia: desconta `billed_seconds` na ordem do ciclo (fixo/móvel ou pooled). Overage só após esgotar. Sobra **não** carrega.

**Concorrência (D-ARQ-17 / D-DB-18):** o worker de rating processa CDRs **por assinatura**, ordenados por `(started_at, legacy_cdr_id)`, sob lock Redis `rating:sub:{subscription_id}` + `pg_advisory_xact_lock(hashtext(subscription_id))` para determinismo de franquia/excedente.

Pró-rata DID/canal:

```
days_remaining / days_in_cycle * unit_cents
```

Half-up para **centavo inteiro** na **linha agregada** de pró-rata no fechamento (imutável na `invoice_lines.snapshot`).

Agregado:

```
consumo_cliente = Σ pró-rata DID + Σ pró-rata canal + Σ minutos sell
cobrado_cliente = package ? mensal + overage : max(consumo_cliente, commitment)
custo_vc1_cliente = mesmas qtds × tarifas da faixa da REVENDA com a VC1
```

---

## 5. Ledger pré-pago

**PADRÃO PROPOSTO: append-only com `balance_after_cents` + `SELECT … FOR UPDATE` na carteira** (D-DB-03). Não é partidas dobradas: um ativo só (crédito do cliente).

Motivo: o produto é carteira de consumo, não contabilidade geral. Idempotência por CDR importa mais que razão contábil. Repasse/fatura usam tabelas próprias (`invoices`, `payouts`), não este ledger.

### 5.1 Fluxo (uma transação)

```sql
BEGIN;
SELECT id, balance_cents, version
  FROM billing.credit_wallets
 WHERE id = :wallet_id
   FOR UPDATE;  -- trava a linha; serializa débitos do mediator

-- recusar se já existe ledger com idempotency_key = 'rated_call:'||rated_call_id
INSERT INTO billing.credit_ledger (
  reseller_id, wallet_id, customer_id, entry_type,
  amount_cents, balance_after_cents, source_type, source_id, idempotency_key
) VALUES (..., 'debit', -sell_cents, balance_cents - sell_cents, 'rated_call', ..., ...);

UPDATE billing.credit_wallets
   SET balance_cents = GREATEST(0, balance_cents - :sell_cents),
       version = version + 1,
       updated_at = now()
 WHERE id = :wallet_id;
-- residual beyond zero → unrecovered_cents on ledger row; subscription blocked_credit
COMMIT;
```

Recarga (`deposit`): top-up is wallet cash only; **does not** waive future `max(consumption, commitment)` at cycle close (D-BIZ-25).

`expire`: zera franquia residual **não** entra no ledger (franquia não é crédito). Ajuste (`adjust`) só papel `vc1_finance` / `reseller_finance`, auditado.

### 5.2 Concorrência

| Mecanismo | Uso |
|-----------|-----|
| `SELECT … FOR UPDATE` na wallet | **Padrão.** Simples, transação curta, cobre mediator + recarga |
| `version` (optimistic) | Detecta retry de app; não substitui o lock |
| Unique `(wallet_id, idempotency_key)` | Impede débito duplo do mesmo CDR |
| Advisory lock `pg_advisory_xact_lock` | Alternativa se o mediator abrir a wallet em outra conexão (D-DB-11) |

Não usar `addCredit` SipPulse.

---

## 6. Repasse e netting (pseudo-SQL)

Modo `vc1_invoices`. Sem split. Só faturas **pagas**. Custo de fatura em aberto **não** conta no mínimo (**PADRÃO PROPOSTO**, D-DB-05; alinhado ao brief).

```sql
-- 1) margem recebida: invoice customer fully paid within cycle window (no JOIN that duplicates rows)
WITH settled_invoices AS (
  SELECT i.id, i.charged_cents AS sell_cents, i.cost_cents
    FROM billing.invoices i
   WHERE i.reseller_id = :rid AND i.cycle_id = :cid
     AND i.party = 'customer' AND i.issuer = 'vc1' AND i.status = 'paid'
     AND EXISTS (
       SELECT 1 FROM billing.payments p
        WHERE p.invoice_id = i.id
          AND p.paid_at >= (SELECT period_start FROM rating.cycles WHERE id = :cid)
          AND p.paid_at <  (SELECT period_end   FROM rating.cycles WHERE id = :cid)
     )
),
margin AS (
  SELECT COALESCE(SUM(sell_cents - cost_cents), 0) AS paid_margin_cents,
         COALESCE(SUM(cost_cents), 0)              AS paid_cost_cents,
         COALESCE(SUM(LEAST(sell_cents - cost_cents, 0)), 0) AS negative_margin_cents
    FROM settled_invoices
),
-- 2) uso próprio: voice + DID + channel from reseller_self (customers.kind=reseller_self)
own AS (
  SELECT COALESCE(SUM(ur.cost_cents), 0)
       + COALESCE(SUM(il.cost_cents), 0) AS own_cost_cents
    FROM customers.customers c
    LEFT JOIN rating.usage_records ur
      ON ur.customer_id = c.id AND ur.reseller_id = :rid AND ur.cycle_id = :cid
    LEFT JOIN billing.invoice_lines il
      ON il.reseller_id = :rid
     AND il.kind IN ('did','channel')
     AND il.invoice_id IN (
           SELECT i.id FROM billing.invoices i
            WHERE i.reseller_id = :rid AND i.cycle_id = :cid
              AND i.customer_id = c.id AND i.party = 'customer'
         )
   WHERE c.reseller_id = :rid AND c.kind = 'reseller_self'
),
params AS (
  SELECT r.commitment_cents FROM tenancy.resellers r WHERE r.id = :rid
)
SELECT
  m.paid_margin_cents,
  GREATEST(0, p.commitment_cents - m.paid_cost_cents - o.own_cost_cents) AS shortfall_cents,
  ABS(m.negative_margin_cents) AS negative_margin_cents,
  m.paid_margin_cents
    - GREATEST(0, p.commitment_cents - m.paid_cost_cents - o.own_cost_cents) AS net_cents
FROM margin m, own o, params p;
```

Exemplo do brief: mínimo 2000; custo pago 1500; margem 600 → shortfall 500 → **net** 100. Margem 300 → shortfall 500 → net −200 → `status=netted_to_invoice` e fatura `party=reseller` de 200. `negative_margin_cents` é informativa; não subtraída de novo.

```sql
-- 3) persistir
INSERT INTO billing.payouts (..., paid_margin_cents, shortfall_cents, negative_margin_cents, net_cents, status)
VALUES (..., :margin, :shortfall, :neg, :net,
        CASE WHEN :net > 0 THEN 'pending_commission_invoice'
             WHEN :net = 0 THEN 'settled_zero'
             ELSE 'netted_to_invoice' END);

-- se net < 0:
INSERT INTO billing.invoices (party='reseller', issuer='vc1', charged_cents=ABS(:net), ...);
```

Modo `reseller_invoices` (exige STFC):

```sql
SELECT GREATEST(
         SUM(i.cost_cents) FILTER (WHERE i.party = 'customer')  -- custo VC1 de todos os clientes
       + own.own_cost_cents,
         r.commitment_cents
       ) AS reseller_invoice_cents
FROM ...;
```

Aqui o custo dos clientes conta **independente** de a revenda ter cobrado o cliente (a VC1 cobra a revenda). Statements saem em `billing.statements`.

---

## 7. Isolamento multi-tenant

1. **Host → reseller:** `{slug}.vc1.com.br` / `reseller_domains.host`. `cliente.vc1.com.br` autentica e redireciona.
2. **Global scope** (obrigatório): trait Eloquent aplica `reseller_id = Tenant::id()` em toda tabela tenant-owned. Staff Filament usa `withoutGlobalScopes()` **só** com `vc1_*` e impersonate auditado.
3. **Índices:** sempre `(reseller_id, …)` na frente para index-only scans por tenant.
4. **FK composta (D-DB-18):** em tabelas tenant-owned, `UNIQUE (id, reseller_id)` no pai; FKs dos filhos `(parent_id, reseller_id) → (id, reseller_id)` em `customers`, `subscriptions`, `proposals`, `invoices`, `statements`, `dids`, `wallets`, `payouts`, etc. (ver `schema.dbml`).
5. **RLS — PADRÃO PROPOSTO opcional** (D-DB-04):

```sql
ALTER TABLE customers.customers ENABLE ROW LEVEL SECURITY;
CREATE POLICY customers_tenant ON customers.customers
  USING (reseller_id = NULLIF(current_setting('app.reseller_id', true), '')::uuid
         OR current_setting('app.is_vc1_staff', true) = 'on');
```

Laravel seta `SET LOCAL app.reseller_id` no middleware. Role de runtime `commerce_app` sem BYPASSRLS; migrator é owner. RLS **não** substitui o scope: é rede de segurança contra query crua.

6. LAB: `integration.lab_allowlist` + decorator `LabGuarded*` — escrita fora da lista recusada **mesmo** com tenant certo.

---

## 8. Mapeamento sales-mework360 (keep / rename / drop)

Fork de `/home/beesy/Projetos/Cursor/suites/mework360/sales-mework360` (`docs/DATABASE.md` + `backend/database/migrations`). PKs passam de `bigint` para `uuid` (alinhamento falepaco-erp; o sales era exceção).

| Tabela / view sales-mework360 | Destino commerce-vc1 | Ação |
|-------------------------------|----------------------|------|
| `users` | `identity.users` | **Rename/replace.** Drop `role` enum único; vira `user_roles`. Drop unique `super_admin`. `manager_id` não migra (hierarquia comercial fica no papel `reseller_seller`) |
| `password_reset_tokens` | `public.password_reset_tokens` | **Keep** (infra Laravel) |
| `sessions` | Redis `SESSION_DRIVER` | **Drop** da PG se Redis; senão keep `public.sessions` |
| `cache` / `cache_locks` | `public.*` | **Keep** infra |
| `jobs` / `job_batches` / `failed_jobs` | `public.*` | **Keep** infra |
| `personal_access_tokens` | `identity.api_tokens` | **Replace** (UUID, `reseller_id`, hash) |
| `plans` (`code` PK, `apps_enabled`, `monthly_price`, `tier_rank`, `storage_gb`, `description`) | `catalog.plans` + `plan_versions` | **Replace** — catálogo MeWork (apps/seats) não existe em voz |
| `sales` | `sales.contracts` + `signature_envelopes` | **Rename/replace.** Status de onboarding MeWork (`provisioned_at`, `tenant_slug`, `account_slug`, `otp_*`, `skip_provisioning`) **drop**. `paco_envelope_id` → `signature_envelopes.mesign_envelope_id`. Snapshot cadastral vai para `contracts.snapshot` |
| `sale_events` | `sales.proposal_events` (e eventos de contrato no mesmo log com `event_type` prefixado) | **Drop** tabela própria; timeline única na proposta/contrato |
| `proposals` | `sales.proposals` | **Keep** (conceito). UUID PK; + `reseller_id`; drop `plan_code`, `plan_items_json`, `admin_user_json`, `mailbox_*`, `demo_*` MeWork, `wizard_mode` Expressa/Completa (substituídos pelo builder de voz), `team_invites_json`, `contract_readiness_json`. Keep PIN/público: `public_access_*`, `slug`→`public_code`, `stage` (enum do brief) |
| `proposal_events` | `sales.proposal_events` | **Keep** + `reseller_id` |
| `customers` | `customers.customers` | **Keep** + `reseller_id`, `legacy_org_id`. Drop `tenant_slug` MeWork |
| `people` | `customers.contacts` | **Rename** (pessoa deixa de ser global) |
| `customer_people` | papel em `customers.contacts.role` | **Drop** pivot; unique `(customer_id, role)` na própria `contacts` |
| view `sales_customer_document_drift` | — | **Drop** (recriar se auditoria precisar: `contracts.snapshot` vs `customers.document`) |
| `migration_service_requests` (+ sync platform) | — | **Drop** (migração mailbox MeWork fora do produto VC1) |
| colunas `platform_onboarding_id`, `demo_current_step`, `seats_*`, `billing_term`, `mail_mode` | — | **Drop** |

Infra que **permanece** no `public`: queue/cache/session. Filament `exports`/`imports` (se o painel VC1 usar) devem apontar `user_id uuid → identity.users` (lição D19 do ERP).

---

## 9. Mapeamento legado e import de CDR

Fontes lidas (não inventar): `easy_cicerone/sql/fpn-592_create_table_projetos_clientes.sql`, `fpn-592_create_table_projetos_clientes_tronco.sql`, insert de `organizacoes` em `api/public/index.php`, queries em `modulo_minutagem/classes/sip_pulse.php` e `api/src/SipPulse/DataBase/DBSipPulse.php`, docs `falepaco-sippulse-old/docs/AUDITORIA-SIPPULSE.md` e `IPS-NECESSARIOS.md`.

### 9.1 Colunas de referência

| Legado `table.column` | commerce `schema.table.column` | Nota |
|----------------------|--------------------------------|------|
| `projetos_clientes.id` | `tenancy.resellers.legacy_project_id` | 1:1 tenant WL |
| `projetos_clientes.nome` | `tenancy.resellers.legal_name` | |
| `projetos_clientes.token` | **não persiste plaintext**; hash se espelhar | Token ERP só no adapter |
| `projetos_clientes.have_tronco` | implícito (sempre tronco) | Cron WP deve skip com flag VC1 |
| `projetos_clientes.domain_sippulse` | `tenancy.resellers.legacy_domain_sippulse` | SIP, não HTTP |
| `projetos_clientes.url` / `url_painel_pabx` | — | HTTP agora é wildcard VC1 |
| `projetos_clientes.domain_dns` | — | Não copiar Google DNS por projeto |
| `projetos_clientes.dia_vencimento` | `tenancy.resellers.due_day` | Vencimento da fatura VC1→revenda; ciclo continua mês calendário |
| `projetos_clientes.bloqueado` | `tenancy.resellers.status=suspended` | |
| `organizacoes.id` | `customers.customers.legacy_org_id` | |
| `organizacoes.id_projeto_cliente` | via `customers.reseller_id` → `legacy_project_id` | |
| `organizacoes.razao_social` | `customers.customers.legal_name` | |
| `organizacoes.nome_fantasia` | `customers.customers.trade_name` | |
| `organizacoes.cpf_cnpj` | `customers.customers.document` | só dígitos |
| `organizacoes.email` | `customers.customers.email` | |
| `organizacoes.cep/endereco/numero/bairro/id_cidade/id_estado` | `customers.addresses.*` | |
| `organizacoes.bloqueio` | `customers.status` / `telco.block_events` | |
| `projetos_clientes_tronco.id` | `customers.customers.legacy_tronco_id` | |
| `projetos_clientes_tronco.id_cliente` | `customers.legacy_org_id` | |
| `projetos_tronco_assinaturas_planos.login` | `telco.sip_accounts.username` | prefixos 750/850/950 |
| `projetos_tronco_planos.id_rate_plan` / `projetos_tronco_planos_disponiveis.id_rate_plan` | `telco.sip_accounts.legacy_rate_plan_id` | enforcement de rede |
| `projetos_tronco_numeros.did_numero` | `telco.dids.e164` | |
| `projetos_tronco_numeros.id` | `telco.dids.legacy_did_id` | |
| `projetos_tronco_portabilidade.id` | `customers.portability_requests.legacy_portability_id` | |
| `projetos_tronco_planos` / `projetos_tronco_planos_disponiveis` | **não** vira catálogo | Catálogo comercial é `catalog.*` |
| `projetos_clientes_titulos` / `projetos_clientes_faturas` | **não migrar SoT** | Cron tronco OFF para projetos VC1 |
| `sippulse.subscriber` / `vi_subscribers.account_code` | `telco.sip_accounts.accountcode` | JOIN CDR |

### 9.2 Import `sippulse_reports.cdrs_report`

Colunas **confirmadas no código** (`sip_pulse.php`, `DBSipPulse.php`, `IPS-NECESSARIOS.md`):

| `cdrs_report` | `rating.rated_calls` / uso | Observação |
|---------------|----------------------------|------------|
| `id` | `legacy_cdr_id` (unique com `started_at`) | Cursor de `cdr_imports` |
| `accountcode` | `accountcode` → lookup `telco.sip_accounts` | Também `vi_subscribers.account_code` via CNPJ no legado — commerce resolve pelo subscriber |
| `call_start_time` | `started_at` | Partição |
| `duration_block` | `switch_duration_block` | Bloco do switch; auditoria apenas |
| `duration` | `raw_seconds` / `switch_duration` | Base do 30/6 comercial |
| `service_type` | `service_type` + map → `destination` | Classificação legado (fixo/móvel). Complementar com prefixo do B-number |
| `service` | metadado jsonb se útil | `DBSipPulse` agrupa por `service` |
| `rate` | reconciliação | Tarifa do switch, não venda |
| `price` | `switch_price_cents` | Unidade (real vs centavo) **A VERIFICAR** |
| `src_ip` | metadado (não fatura) | Docs IPS |
| `dst_uri` | metadado; no legado sempre domínio do proxy | Não usar como B-number |

Colunas de A/B number (`caller`, `callee`, `dst`, `src`…) **não aparecem** nas queries do ERP lidas — **A VERIFICAR** no `SHOW COLUMNS` em LAB (sem credencial neste blueprint). Sem B-number, classificar por `service_type` até o inventário.

Filtro do import:

```
accountcode IN (sip_accounts.accountcode da allowlist LAB / do tenant)
AND call_start_time >= cursor
AND id > cursor_after_id
```

Chamadas `destination=other`: **PADRÃO PROPOSTO** bloquear originação e não faturar (D-DB-08).

---

## 10. Volumes, retenção, backup

| Série | Volume | Fonte |
|-------|--------|--------|
| CDR SipPulse | ~52 mil chamadas/dia ≈ **1,6 M/mês** | `_BRIEF.md`; `AUDITORIA-SIPPULSE.md` (8,8 M linhas históricas) |
| `rated_calls` | ~1,6 M linhas/mês × 5 anos fiscal ≈ 96 M (mas CDR raw só 13 meses) | |
| `usage_records` | O(assinaturas × 2 destinos × meses) — milhares, não milhões | |
| Faturas | O(clientes × meses) | |

**PADRÃO PROPOSTO de retenção** (D-DB-06, D-DB-07):

| Dado | Retenção | Ação |
|------|----------|------|
| `rated_calls` + `invoice_lines` + `invoices` + `fiscal_*` + `payments` + `payouts` + `contracts.snapshot` | **5 anos** (fiscal/billing) | Partições mensais; drop só após 60 meses + lock fiscal |
| CDR bruto / colunas `switch_*` / cópia operacional do import | **13 meses** | Após 13 meses nulificar `switch_price_cents`/`source_number` ou arquivar partição em object storage; manter `sell_cents`/`billed_seconds`/`legacy_cdr_id` pelos 5 anos |
| `credit_ledger` | 5 anos | |
| `audit_log` | 5 anos | sem purge automático antes |
| `outbox` publicado | 30 dias | prune |
| `inbox_webhooks` | 90 dias | |
| `portability_files` | `retained_until` (PADRÃO PROPOSTO 24 meses após conclusão, D-DB-12) | job LGPD |
| `connector_calls` | 90 dias | |

**Backup:** PG Tier 1 meCloud — `pg_basebackup` + WAL archive off-host (mesmo espírito falepaco-erp). PITR. Restore testado no LAB antes de piloto. RPO alvo **PADRÃO PROPOSTO** 15 min (`archive_timeout`), RTO manual. Filas Redis **não** são SoT. Object storage dos `files.objects` versionado à parte.

Partições: `rating.rated_calls` e `rating.usage_records` `PARTITION BY RANGE` mensal. Criar `N+2` meses à frente no `cycle_runs`. Sem particionar `invoices` na v1.

---

## 11. Ordem de migrations

Uma migration Laravel por passo (transacional). Schemas criados primeiro.

| # | Migration | Conteúdo |
|---|-----------|----------|
| 01 | `create_schemas` | `identity`, `tenancy`, `pricing`, `catalog`, `sales`, `customers`, `telco`, `rating`, `billing`, `fiscal`, `integration`, `audit`, `notifications`, `files` |
| 02 | `create_enums` | Todos os ENUM do DBML |
| 03 | `identity_*` | users, roles, permissions, user_roles, role_permissions, api_tokens |
| 04 | `tenancy_resellers` | resellers (FKs de faixa nullable até 05) |
| 05 | `pricing_*` | bases, versions, tiers, version_rows, reseller tables/rows; então FK `resellers.vc1_tier_id` |
| 06 | `tenancy_rest` | domains, branding, stfc, settings, contracts |
| 07 | `catalog_*` | destinations, suggested_packages, plans, versions, quotas, trial_rules |
| 08 | `customers_*` | customers, addresses, contacts, customer_users (subscriptions depois de plans) |
| 09 | `sales_*` | proposals, items, events, trials, contracts, envelopes |
| 10 | `customers_subscriptions` | subscriptions + FKs cruzadas `source_proposal_id` |
| 11 | `telco_*` | sip_accounts, dids, allocations, changes, block_events |
| 12 | `rating_cycles_prefixes_imports` | |
| 13 | `rating_usage_records_parent` | PARTITION BY RANGE + 3 partições iniciais |
| 14 | `rating_rated_calls_parent` | idem |
| 15 | `billing_credit_*` | wallets, ledger, limits, events |
| 16 | `billing_invoices_*` | methods, invoices, lines, statements, payments, dunning, cycle_runs |
| 17 | `billing_payouts_*` | payouts, lines, commission_invoices |
| 18 | `fiscal_*` | |
| 19 | `integration_*` | outbox, inbox, connector_calls, legacy_id_map, reconciliation, lab_allowlist |
| 20 | `audit_notifications_files` | |
| 21 | `drop_mework_tables` | `plans`, `sales`, `sale_events`, `people`, `customer_people`, `migration_service_requests`, view drift — **só no fork**, nunca no repo origin |
| 22 | `rls_policies` | opcional, feature flag (D-DB-04) |
| 23 | seeders | §12 |

Não migrar dados de produção MeWork de mailbox. Funil voz nasce vazio. ETL legado é **sob demanda** via `legacy_id_map` (piloto), não cutover big-bang.

---

## 12. Seed

### 12.1 Papéis (`identity.roles`)

| slug | scope |
|------|--------|
| `vc1_admin` | vc1 |
| `vc1_finance` | vc1 |
| `vc1_ops` | vc1 |
| `vc1_support` | vc1 |
| `reseller_owner` | reseller |
| `reseller_admin` | reseller |
| `reseller_finance` | reseller |
| `reseller_seller` | reseller |
| `customer_admin` | customer |
| `customer_user` | customer |

### 12.2 Slugs de host reservados

`admin`, `cliente`, `api`, `sales`, `lab`, `www`, `sip`, `mail`, `status`, `docs`, `app`, `static` — CHECK na aplicação + teste; **não** inserir `tenancy.resellers` com esses slugs.

### 12.3 Destinos

| code | billable | blocked_by_default |
|------|----------|--------------------|
| `landline_br` | true | false |
| `mobile_br` | true | false |
| `other` | false | **true** (PADRÃO PROPOSTO) |

Prefixos: seed mínimo **A VERIFICAR** (móvel BR 9 dígitos após DDD; fixo demais). Até lá, `service_type` do CDR.

### 12.4 Cadência e livro de preços

Uma `pricing.price_bases` `code=vc1-default`, versão 1 publicada, faixas ilustrativas **sem valores de produção** (operador cadastra no painel). `cadence_min_s=30`, `cadence_inc_s=6`.

### 12.5 Revenda demo LAB

| Campo | Valor |
|-------|--------|
| slug | `lab` é reservado — usar `demo` |
| legal_name | `VC1 Demo LAB` |
| billing_mode | `vc1_invoices` |
| status | `active` |
| Host | `demo.lab.vc1.com.br` |
| `legacy_project_id` | ID do projeto WL demo **na allowlist** (preenchido só com aprovação do operador) |
| `integration.lab_allowlist` | project + org + accountcodes + DIDs demo |
| Usuário | `reseller_owner` `demo@lab.vc1.local` (senha só no seeder local, nunca commitada) |
| Cliente `reseller_self` | 1 subscription opcional para uso próprio |

Nenhuma escrita SOAP fora da allowlist.

---

## 13. Itens para 10-DECISIONS

| ID | Linha | Default (PADRÃO PROPOSTO) |
|----|-------|---------------------------|
| D-DB-01 | Tipo da tarifa por minuto | `*_cents_per_min numeric(12,4)` (centavos de real / minuto). Recusa millicents inteiros: 4 casas cobrem R$ 0,000001/s efetivos na fórmula 30/6 e permanecem legíveis ao lado de `*_cents bigint` |
| D-DB-02 | Arredondamento half-up | **Só na linha agregada** da fatura (por classe de destino × assinatura × ciclo). `rated_calls.sell_amount_cents` / `cost_amount_cents` = `numeric(14,4)` por chamada |
| D-DB-03 | Ledger pré-pago | Append-only + `balance_after_cents` + `SELECT … FOR UPDATE` na wallet. Não partidas dobradas |
| D-DB-04 | RLS | Opcional, desligado no MVP; scopes Eloquent obrigatórios. Ligar RLS no piloto E3 se houver query crua |
| D-DB-05 | Custo inadimplente no mínimo (`vc1_invoices`) | **Não conta** até a fatura do cliente estar `paid` (já no brief) |
| D-DB-06 | Retenção CDR raw / `switch_*` | **13 meses**; átomos comerciais 5 anos |
| D-DB-07 | Retenção fiscal/billing | **5 anos** (faturas, linhas, NFCom, pagamentos, contratos, payouts) |
| D-DB-08 | Destino `other` | Bloqueado e não faturado por padrão |
| D-DB-09 | Partição | `RANGE` mensal em `rated_calls(started_at)` e `usage_records(period_start)`; PK composta `(id, partition_key)` |
| D-DB-10 | Pagamento de repasse | Registro manual de TED/Pix pela tesouraria VC1 até mePay oferecer payout |
| D-DB-11 | Lock alternativo do ledger | Só `pg_advisory_xact_lock` se o mediator não puder usar a mesma transação da wallet |
| D-DB-12 | TTL docs de portabilidade | 24 meses após `completed_at`; disco `private` |
| D-DB-13 | Âncora do ciclo | Mês calendário `America/Sao_Paulo` para todos; `due_day` da revenda (default 10) só data de vencimento |
| D-DB-14 | Prefixos fixo/móvel | Inventário ANATEL em LAB (**A VERIFICAR**); fallback `service_type` do CDR até lá |
| D-DB-15 | Campo de duração CDR | Tarifar em `duration` (segundos atendidos); guardar `duration_block` para reconciliação; 30/6 só no PG |
| D-DB-16 | Uso próprio da revenda | Sempre existe `customers.kind=reseller_self` (pode ficar sem SIP até ativar tronco próprio) |
| D-DB-17 | Drop das tabelas MeWork no fork | Executar na migration 21 só depois do seeder de papéis; sem dual-write |
| D-DB-18 | Integridade composta tenant | `UNIQUE (id, reseller_id)` + FKs compostas nos filhos |
| D-DB-19 | Idempotência fechamento | Partial uniques: `invoices` `(cycle_id, reseller_id)` WHERE party=reseller AND status NOT IN (cancelled,reissued); `(cycle_id, subscription_id)` WHERE party=customer AND status NOT IN (cancelled,reissued); `statements` UNIQUE `(cycle_id, subscription_id)`; `cycle_runs(cycle_id, phase)`; fiscal ativo/invoice |
| D-DB-20 | `public_code` proposta | Único **global**; base32 aleatório ≥ 10 chars |
| D-DB-21 | `party=customer` | CHECK proíbe `issuer=reseller` em `invoices`; modo revenda fatura usa só `statements` |
| D-DB-22 | Trial | Único vitalício `(reseller_id, customer_id, plan_id)`; `plan_id` denormalizado em `sales.trials` |
