Phase 03 — Product Delivery
HITLs #7-#10 Approved Architect + Back End + Front End + QA ← Ecosystem 🇧🇷 PT

Phase 03 — Product Delivery

Architecture, ADRs, API, database, business rules, security and QA.

6
Bounded Contexts
6
ADRs
29
Endpoints
10
Tables
8
Admin Pages
82/100
QA Score

Architecture — Provider Pattern

ECP Pay uses the Provider Pattern to abstract payment gateways. Ecosystem apps call a single API — the factory selects the active adapter (Internal or Asaas) via feature flag, without the apps knowing which is active.

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

Payment Processing
Core domain: processes Pix, Card and Boleto via provider abstraction layer. Aggregates: Transaction, Refund.
Provider Abstraction
Provider Pattern — abstracts internal simulation vs. external gateway (Asaas). Switchable via feature flag at runtime.
Card Vault
Tokenized card storage. Never persists full numbers. Reusable tokens cross-app.
Split Engine
Payment split logic for multi-party transactions (platform + seller + delivery).
Webhook & Callback
Inbound webhook processing (Asaas) and outbound callback delivery (apps) with retry and backoff.
Admin Panel
Support subdomain: dashboard, transaction management, provider config, feature flags, audit logs.
architecture-output.json

Architecture Decision Records (ADRs)

ADR-001
Provider Pattern for payment gateway abstraction
Accepted
Context: Ecosystem needs to work offline (dev/staging) and with real gateways (production). Apps must not know which gateway is active.
Decision: PaymentProvider interface with two adapters (InternalAdapter, AsaasAdapter). Factory selects via feature flag PAYMENT_PROVIDER, switchable at runtime via admin panel.
Consequences: Adding a new gateway = implementing one adapter. Zero changes in consumer apps.
ADR-002
SQLite as embedded single-file database
Accepted
Context: ECP Pay is a monorepo service for local dev, staging and small-scale production. Needs zero-config database.
Decision: better-sqlite3 with WAL mode. Single file database-pay.sqlite. Migrations run at startup.
Consequences: No external DB server. One npm install and the service runs. Trade-off: not suitable for high-scale horizontal deployment.
ADR-003
Dual authentication: API Key for apps, JWT for admin
Accepted
Context: Two distinct user groups: ecosystem apps (machine-to-machine) and human admins (web panel).
Decision: Apps authenticate via X-API-Key. Admin via JWT issued at login. Separate middlewares.
Consequences: Clean separation of concerns. Apps never need user credentials.
ADR-004
Idempotency via unique key per transaction
Accepted
Context: Network failures can cause duplicate payment requests. Must guarantee payment is created exactly once.
Decision: Mandatory X-Idempotency-Key header on all mutation endpoints. Stored as UNIQUE in transactions table. Duplicate returns existing result.
Consequences: Safe retries for all apps. UNIQUE constraint prevents double-charging at database level.
ADR-005
Card data never persisted — token-only vault
Accepted
Context: PCI compliance and security: card numbers and CVVs must not be stored.
Decision: Only token + last4 + brand + holder_name in card_tokens table. Raw data passed to provider and immediately discarded.
Consequences: Secure by design. Reusable token cross-app. Trade-off: cannot display full card number.
ADR-006
Webhook deduplication via event_id
Accepted
Context: External providers (Asaas) may resend webhooks, causing duplicate processing.
Decision: event_id as UNIQUE in webhook_events table. Check before processing. If duplicate, return 200 without reprocessing.
Consequences: Idempotent webhook handling. No duplicate status changes or callbacks.

Sequence Diagrams

Pix Payment — Full Flow (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: Validate API Key (app_registrations)
    Pay->>DB: SELECT WHERE idempotency_key = ?
    DB-->>Pay: null (new)

    Pay->>Svc: createPixPayment(data)
    Svc->>Prov: ProviderFactory.get("internal")
    Prov->>Prov: Generate QR Code + copy-paste

    Note over Svc,DB: Atomic transaction
    Svc->>DB: INSERT transactions (status: pending)
    Svc->>DB: INSERT splits (if defined)
    DB-->>Svc: transactionId

    Prov->>Prov: Configurable delay (3-5s)
    Prov->>DB: UPDATE transactions SET status = paid

    Svc->>App: POST callback_url (status: paid)
    Note over App: Webhook delivery with retry

    Pay-->>App: { id, status: pending, qrCode, copyPaste }
      

Card Payment — Asaas Provider (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: Validate API Key + Idempotency
    Pay->>Svc: createCardPayment(data)
    Svc->>Prov: ProviderFactory.get("external")

    Prov->>DB: SELECT card_tokens WHERE customer_document
    alt New card
        Prov->>Asaas: POST /payments (card data)
        Asaas-->>Prov: { id, status, token }
        Prov->>DB: INSERT card_tokens (token, last4, brand)
    else Tokenized card
        Prov->>Asaas: POST /payments (token)
        Asaas-->>Prov: { id, status }
    end

    Svc->>DB: INSERT transactions (status: pending)

    Note over Asaas,Pay: Async webhook
    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)
      

Provider Switch — 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: Opens Providers page
    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 with toggle (Internal active)

    Admin->>Panel: Clicks "Activate External"
    Panel-->>Admin: Confirmation modal
    Admin->>Panel: Confirms 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: Updated banner + confirmation toast
    Note over Admin: All new transactions use Asaas
      

Webhook Delivery — Exponential Backoff Retry

sequenceDiagram
    participant Pay as ECP Pay
    participant DB as SQLite
    participant App as App callback_url

    Note over Pay,App: Transaction status changed

    Pay->>DB: SELECT callback_url FROM transactions
    Pay->>App: POST callback_url (attempt 1)

    alt Success (2xx)
        App-->>Pay: 200 OK
        Pay->>DB: UPDATE: delivery_status = delivered
    else Failure (timeout/5xx)
        App-->>Pay: 500 / timeout

        Note over Pay: Retry 1: wait 30s
        Pay->>App: POST callback_url (attempt 2)
        App-->>Pay: 500 / timeout

        Note over Pay: Retry 2: wait 2min
        Pay->>App: POST callback_url (attempt 3)
        App-->>Pay: 500 / timeout

        Note over Pay: Retry 3: wait 10min
        Pay->>App: POST callback_url (attempt 4)

        alt Success
            App-->>Pay: 200 OK
            Pay->>DB: UPDATE: delivery_status = delivered
        else Final failure
            Pay->>DB: UPDATE: delivery_status = failed
            Note over Pay: Admin can perform manual retry
        end
    end
      

API Endpoints

Payment API (app-facing) — /pay

Authentication via X-API-Key header. 9 endpoints.

POST/pay/pix — Create Pix charge (QR code + copy-paste)
POST/pay/card — Charge credit card (new or tokenized)
POST/pay/boleto — Issue boleto with barcode + Pix QR
GET/pay/transactions/:id — Transaction status
POST/pay/transactions/:id/refund — Full or partial refund
GET/pay/cards/:customer_document — List saved cards
DELETE/pay/cards/tokens/:token_id — Remove card token
POST/pay/webhooks/asaas — Receive Asaas webhook
GET/pay/health — Health check + active provider

Admin API (web panel) — /admin

Authentication via JWT Bearer token. 20 endpoints.

POST/admin/auth/login — Admin login
GET/admin/auth/me — Current admin user
GET/admin/dashboard — KPIs and aggregated data
GET/admin/transactions — Paginated transaction list
GET/admin/transactions/summary — Summary by type/app/period
GET/admin/transactions/:id — Transaction detail
POST/admin/transactions/:id/simulate-payment — Simulate payment (internal)
GET/admin/providers — Active provider info
POST/admin/providers/switch — Switch internal/external
GET/admin/feature-flags — List feature flags
PATCH/admin/feature-flags/:key — Update feature flag
GET/admin/config — General configuration
PATCH/admin/config — Update configuration
GET/admin/apps — Registered apps
POST/admin/apps — Register new app
PATCH/admin/apps/:id — Update app config
GET/admin/webhooks — List webhooks
POST/admin/webhooks/:id/retry — Manual webhook retry
GET/admin/audit-logs — Audit logs
GET/admin/splits — List transaction splits
architecture-output.json

Database Schema

10 tables + 16 indexes. SQLite with WAL mode via better-sqlite3. Migration: 001-initial.sql.

TableDescriptionKey Columns
transactionsCentral registry of all transactions (Pix, Card, Boleto)id, type, status, amount, source_app, idempotency_key, provider, callback_url
refundsFull or partial refunds linked to transactionsid, transaction_id, amount, status, reason
card_tokensToken vault (never raw data). Cross-app via customer_documentid, customer_document, token, last4, brand, holder_name, is_active
splitsSplit rules per transaction (platform, seller, delivery)id, transaction_id, account_id, amount, type
webhook_eventsWebhooks received from provider (Asaas). Dedup via UNIQUE event_idid, event_id, event_type, payload, processed, transaction_id
app_registrationsRegistered ecosystem apps with API key and callback URLid, app_name, api_key, callback_base_url, is_active
admin_usersAdmin panel users with roles (admin, operator, viewer)id, name, email, password_hash, role
feature_flagsRuntime-editable feature flags (PAYMENT_PROVIDER, etc.)id, key, value, description
audit_logsAudit log of all administrative actionsid, user_id, action, resource, metadata, ip_address, timestamp
configGeneral system configuration (simulation delay, etc.)id, key, value, description

Entity-Relationship Diagram

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"
    

Business Rules

BR-01
Idempotency — Every transaction requires a UUID idempotency_key. Resend returns existing result.
BR-02
Amounts in cents — All monetary values in cents (integer). NEVER float.
BR-03
Provider flag — PAYMENT_PROVIDER (internal/external) switchable at runtime via admin panel.
BR-04
Internal: Pix auto-approve — Pix auto-approved after configurable delay (default 3-5s).
BR-05
Internal: Card approved — Approved if amount < R$10,000 and last4 ≠ 9999.
BR-06
Internal: Card declined — Declined if last4 = 9999 (CARD_DECLINED) or amount > R$10,000 (LIMIT_EXCEEDED).
BR-07
Internal: Mock boleto — Boleto with mock FEBRABAN barcode. Payable via button in admin panel.
BR-08
Token vault — Token linked to CPF/CNPJ. Reusable cross-app. Never stores raw data.
BR-09
Split — Sum of parts MUST equal total transaction. Mandatory validation.
BR-10
Webhook retry — 3 attempts with exponential backoff: 30s, 2min, 10min. After 3 failures: delivery_failed.
BR-11
Soft delete — No record is physically deleted. Uses is_active/deleted_at.
BR-12
Audit log — Every administrative action logged with user_id, action, resource, IP and timestamp.
BR-13
Rate limiting — 100 transactions/minute per app. X-RateLimit-* headers in responses.
BR-14
Service-to-service auth — Apps authenticate via X-API-Key validated against app_registrations.
BR-15
Panel auth — JWT with roles: admin (full), operator (transactions + webhooks), viewer (read-only).
BR-16
Card data — Full number and CVV are NEVER persisted. Only token, last4 and brand.
BR-17
Refund — Card and Pix allow refund (full or partial). Boleto does not allow refund.
BR-18
Callback URL — Per-transaction callback URL overrides app default. Fallback to callback_base_url.

Security Model

App Authentication
  • X-API-Key header validated against app_registrations
  • X-Source-App header validated against registered app
  • Rate limiting: 100 tx/min per app
  • Inactive app returns 403
Admin Authentication
  • Login via email/password with bcrypt
  • JWT with 24h expiration
  • 3 roles: admin, operator, viewer
  • requireRole() middleware for RBAC
Data Protection
  • Card vault: only token + last4 + brand
  • Full number and CVV never persisted
  • Soft delete on all records
  • Audit log for traceability
Transactional Integrity
  • Idempotency key UNIQUE in database
  • Webhook dedup via UNIQUE event_id
  • Values always in cents (integer)
  • UUID v4 for all primary keys

QA Report

Quality Score
82/100 — Approved with observations approved

ECP Pay v1 is solid and production-ready in internal mode. The core payment flow (Pix, Card, Boleto), provider pattern, security model, database schema and admin panel are well built.

Identified gaps: 4 missing admin endpoints (splits, tokens, webhooks admin, webhook retry), 3 missing frontend pages (splits, card-vault, audit-log as standalone routes), and minor business rule gaps. None block v1 launch in internal mode.

Business rules: 14/18 implemented, 3 partial, 1 missing Technical rules: 6/6 implemented
Business Rule Coverage

Implemented (14): BR-01 to BR-08, BR-10 to BR-16

Partial (3): BR-09 (split without flow integration), BR-17 (refund without 90-day limit), BR-18 (incomplete callback URL fallback)

Missing (1): 90-day refund limit (BR-17)

API Coverage

Payment endpoints: 9/9 implemented (100%)

Admin endpoints: 17/20 implemented (85%)

Total: 26/29 implemented (90%)

Missing: GET /admin/splits, GET /admin/webhooks, POST /admin/webhooks/:id/retry

qa-report.json backend-status.json frontend-status.json