Fase 03 — Product Delivery
Arquitetura, ADRs, API, banco de dados, regras de negócio, segurança e QA.
Arquitetura — Provider Pattern
O ECP Pay usa o Provider Pattern para abstrair gateways de pagamento. Apps do ecossistema chamam uma única API — a factory seleciona o adapter ativo (Internal ou Asaas) via feature flag, sem que os apps saibam qual está ativo.
graph TD
A["ecp-bank :3333"] -->|X-API-Key| B["ECP Pay API :3335"]
C["ecp-emps :3334"] -->|X-API-Key| B
D["ecp-food :3000"] -->|X-API-Key| B
B --> E["Payment Service"]
E --> F["Provider Factory"]
F -->|"feature flag"| G["Internal Adapter"]
F -->|"feature flag"| H["Asaas Adapter"]
G --> I[("SQLite")]
H --> J["Asaas API"]
B --> K["Admin Panel :5176"]
Bounded Contexts
Architecture Decision Records (ADRs)
Decisão: Interface PaymentProvider com dois adapters (InternalAdapter, AsaasAdapter). Factory seleciona via feature flag PAYMENT_PROVIDER, switchable em runtime via admin panel.
Consequências: Adicionar novo gateway = implementar um adapter. Zero mudanças nos apps consumidores.
Decisão: better-sqlite3 com WAL mode. Arquivo único database-pay.sqlite. Migrations rodam no startup.
Consequências: Sem servidor de DB externo. Um npm install e o serviço roda. Trade-off: não adequado para deploy horizontal de alta escala.
Decisão: Apps autenticam via X-API-Key. Admin via JWT emitido no login. Middlewares separados.
Consequências: Separação limpa de concerns. Apps nunca precisam de credenciais de usuário.
Decisão: Header X-Idempotency-Key obrigatório em todos os endpoints de mutação. Armazenado como UNIQUE na tabela transactions. Duplicata retorna resultado existente.
Consequências: Retries seguros para todos os apps. UNIQUE constraint previne double-charging no nível do banco.
Decisão: Apenas token + last4 + brand + holder_name na tabela card_tokens. Dados raw passados ao provider e descartados imediatamente.
Consequências: Seguro by design. Token reutilizável cross-app. Trade-off: não pode exibir número completo do cartão.
Decisão: event_id como UNIQUE na tabela webhook_events. Verifica antes de processar. Se duplicata, retorna 200 sem reprocessar.
Consequências: Handling idempotente de webhooks. Sem mudanças de status ou callbacks duplicados.
Diagramas de Sequência
Pagamento Pix — Fluxo Completo (Internal Provider)
sequenceDiagram
participant App as App (ecp-food)
participant Pay as ECP Pay API :3335
participant Svc as Payment Service
participant Prov as Internal Adapter
participant DB as SQLite
App->>Pay: POST /pay/pix
Note over Pay: Headers: X-API-Key, X-Idempotency-Key
Pay->>Pay: Validar API Key (app_registrations)
Pay->>DB: SELECT WHERE idempotency_key = ?
DB-->>Pay: null (novo)
Pay->>Svc: createPixPayment(data)
Svc->>Prov: ProviderFactory.get("internal")
Prov->>Prov: Gerar QR Code + copia-e-cola
Note over Svc,DB: Transação atômica
Svc->>DB: INSERT transactions (status: pending)
Svc->>DB: INSERT splits (se definido)
DB-->>Svc: transactionId
Prov->>Prov: Delay configurável (3-5s)
Prov->>DB: UPDATE transactions SET status = paid
Svc->>App: POST callback_url (status: paid)
Note over App: Webhook delivery com retry
Pay-->>App: { id, status: pending, qrCode, copyPaste }
Pagamento Cartão — Provider Asaas (External)
sequenceDiagram
participant App as App (ecp-bank)
participant Pay as ECP Pay API
participant Svc as Payment Service
participant Prov as Asaas Adapter
participant Asaas as Asaas API
participant DB as SQLite
App->>Pay: POST /pay/card
Note over Pay: { amount, card: {number, cvv, ...}, customer }
Pay->>Pay: Validar API Key + Idempotency
Pay->>Svc: createCardPayment(data)
Svc->>Prov: ProviderFactory.get("external")
Prov->>DB: SELECT card_tokens WHERE customer_document
alt Cartão novo
Prov->>Asaas: POST /payments (card data)
Asaas-->>Prov: { id, status, token }
Prov->>DB: INSERT card_tokens (token, last4, brand)
else Cartão tokenizado
Prov->>Asaas: POST /payments (token)
Asaas-->>Prov: { id, status }
end
Svc->>DB: INSERT transactions (status: pending)
Note over Asaas,Pay: Webhook assíncrono
Asaas->>Pay: POST /pay/webhooks/asaas
Pay->>DB: INSERT webhook_events (dedup event_id)
Pay->>DB: UPDATE transactions SET status = paid
Pay->>App: POST callback_url (status: paid)
Switch de Provider — Internal ↔ External
sequenceDiagram
participant Admin as Admin (Browser)
participant Panel as Admin Panel :5176
participant API as Admin API
participant DB as SQLite
Admin->>Panel: Abre página Providers
Panel->>API: GET /admin/providers
API->>DB: SELECT feature_flags WHERE key = PAYMENT_PROVIDER
DB-->>API: { value: "internal" }
API-->>Panel: { active: "internal", available: ["internal", "external"] }
Panel-->>Admin: Two-card com toggle (Internal ativo)
Admin->>Panel: Clica "Ativar External"
Panel-->>Admin: Modal de confirmação
Admin->>Panel: Confirma switch
Panel->>API: POST /admin/providers/switch
Note over API: Middleware: requireRole("admin")
API->>DB: UPDATE feature_flags SET value = "external"
API->>DB: INSERT audit_logs (action: provider_switch)
API-->>Panel: { active: "external", switchedAt }
Panel-->>Admin: Banner atualizado + toast de confirmação
Note over Admin: Todas as novas transações usam Asaas
Webhook Delivery — Retry com Backoff Exponencial
sequenceDiagram
participant Pay as ECP Pay
participant DB as SQLite
participant App as App callback_url
Note over Pay,App: Status de transação mudou
Pay->>DB: SELECT callback_url FROM transactions
Pay->>App: POST callback_url (attempt 1)
alt Sucesso (2xx)
App-->>Pay: 200 OK
Pay->>DB: UPDATE: delivery_status = delivered
else Falha (timeout/5xx)
App-->>Pay: 500 / timeout
Note over Pay: Retry 1: aguarda 30s
Pay->>App: POST callback_url (attempt 2)
App-->>Pay: 500 / timeout
Note over Pay: Retry 2: aguarda 2min
Pay->>App: POST callback_url (attempt 3)
App-->>Pay: 500 / timeout
Note over Pay: Retry 3: aguarda 10min
Pay->>App: POST callback_url (attempt 4)
alt Sucesso
App-->>Pay: 200 OK
Pay->>DB: UPDATE: delivery_status = delivered
else Falha final
Pay->>DB: UPDATE: delivery_status = failed
Note over Pay: Admin pode fazer retry manual
end
end
API Endpoints
Payment API (app-facing) — /pay
Autenticação via X-API-Key header. 9 endpoints.
Admin API (web panel) — /admin
Autenticação via JWT Bearer token. 20 endpoints.
Schema do Banco de Dados
10 tabelas + 16 índices. SQLite com WAL mode via better-sqlite3. Migration: 001-initial.sql.
| Tabela | Descrição | Colunas-Chave |
|---|---|---|
| transactions | Registro central de todas as transações (Pix, Card, Boleto) | id, type, status, amount, source_app, idempotency_key, provider, callback_url |
| refunds | Estornos totais ou parciais vinculados a transações | id, transaction_id, amount, status, reason |
| card_tokens | Cofre de tokens (nunca dados raw). Cross-app via customer_document | id, customer_document, token, last4, brand, holder_name, is_active |
| splits | Regras de split por transação (plataforma, vendedor, entrega) | id, transaction_id, account_id, amount, type |
| webhook_events | Webhooks recebidos do provider (Asaas). Dedup via event_id UNIQUE | id, event_id, event_type, payload, processed, transaction_id |
| app_registrations | Apps do ecossistema registrados com API key e callback URL | id, app_name, api_key, callback_base_url, is_active |
| admin_users | Usuários do painel admin com roles (admin, operator, viewer) | id, name, email, password_hash, role |
| feature_flags | Feature flags editáveis em runtime (PAYMENT_PROVIDER, etc.) | id, key, value, description |
| audit_logs | Log de auditoria de todas as ações administrativas | id, user_id, action, resource, metadata, ip_address, timestamp |
| config | Configurações gerais do sistema (simulation delay, etc.) | id, key, value, description |
Diagrama Entidade-Relacionamento
erDiagram
transactions {
TEXT id PK
TEXT type "pix|card|boleto"
TEXT status "pending|paid|failed|refunded|cancelled"
INTEGER amount_cents
TEXT source_app FK
TEXT idempotency_key UK
TEXT provider "internal|asaas"
TEXT external_id
TEXT callback_url
TEXT delivery_status
TEXT customer_name
TEXT customer_document
TEXT metadata
TEXT created_at
}
refunds {
TEXT id PK
TEXT transaction_id FK
INTEGER amount_cents
TEXT status
TEXT reason
TEXT created_at
}
card_tokens {
TEXT id PK
TEXT customer_document
TEXT token UK
TEXT last4
TEXT brand
TEXT holder_name
INTEGER is_active
}
splits {
TEXT id PK
TEXT transaction_id FK
TEXT account_id
INTEGER amount_cents
TEXT type "platform|seller|delivery"
}
webhook_events {
TEXT id PK
TEXT event_id UK
TEXT event_type
TEXT payload
INTEGER processed
TEXT transaction_id FK
TEXT created_at
}
app_registrations {
TEXT id PK
TEXT app_name UK
TEXT api_key UK
TEXT callback_base_url
INTEGER is_active
}
admin_users {
TEXT id PK
TEXT name
TEXT email UK
TEXT password_hash
TEXT role "admin|operator|viewer"
}
feature_flags {
TEXT id PK
TEXT key UK
TEXT value
TEXT description
}
audit_logs {
TEXT id PK
TEXT user_id FK
TEXT action
TEXT resource
TEXT metadata
TEXT ip_address
TEXT created_at
}
config {
TEXT id PK
TEXT key UK
TEXT value
TEXT description
}
transactions ||--o{ refunds : "has"
transactions ||--o{ splits : "has"
transactions ||--o{ webhook_events : "triggers"
app_registrations ||--o{ transactions : "creates"
admin_users ||--o{ audit_logs : "performs"
Regras de Negócio
Modelo de Segurança
- X-API-Key header validado contra
app_registrations - X-Source-App header validado contra o app registrado
- Rate limiting: 100 tx/min por app
- App inativo retorna 403
- Login via email/senha com bcrypt
- JWT com 24h de expiração
- 3 roles: admin, operator, viewer
- Middleware requireRole() para RBAC
- Card vault: apenas token + last4 + brand
- Número completo e CVV nunca persistidos
- Soft delete em todos os registros
- Audit log para rastreabilidade
- Idempotency key UNIQUE no banco
- Webhook dedup via event_id UNIQUE
- Valores sempre em centavos (integer)
- UUID v4 para todos os primary keys
Relatório de QA
ECP Pay v1 está sólido e pronto para produção em modo internal. O fluxo central de pagamento (Pix, Card, Boleto), provider pattern, modelo de segurança, schema do banco e painel admin estão bem construídos.
Gaps identificados: 4 endpoints admin faltantes (splits, tokens, webhooks admin, webhook retry), 3 páginas frontend faltantes (splits, card-vault, audit-log como rotas standalone), e gaps menores em regras de negócio. Nenhum bloqueia o lançamento v1 em modo internal.
Implementadas (14): RN-01 a RN-08, RN-10 a RN-16
Parciais (3): RN-09 (split sem integração no fluxo), RN-17 (estorno sem limite 90 dias), RN-18 (fallback de callback URL incompleto)
Faltante (1): Limite de 90 dias para estorno (RN-17)
Payment endpoints: 9/9 implementados (100%)
Admin endpoints: 17/20 implementados (85%)
Total: 26/29 implementados (90%)
Faltantes: GET /admin/splits, GET /admin/webhooks, POST /admin/webhooks/:id/retry