Fluxo ponta a ponta

Esta página mostra quem fala com quem, em que ordem, e onde cada garantia (idempotência, fetch-back, reserva) acontece. Os detalhes de implementação estão em deposito.md, saque.md e webhook-e-jobs.md.

1. Componentes e fronteiras

flowchart LR
  J["Jogador: navegador ou app"] -->|"sessão da bet, cookie HttpOnly"| F["nero_client (front)"]
  F -->|"HTTPS, sem segredo no bundle"| P["nero_portal :8080"]
  P -->|"X-Nero-Identity (asserção assinada)"| C["nero_payments (conector + outbox dracma_intents)"]
  C <-->|"reserva, crédito, devolução"| L["nero_ledger (carteira)"]
  C -->|"mTLS + HMAC + Idempotency-Key"| D["Dracma API /v1"]
  D -->|"credencial de PSP, nunca sai do Dracma"| PSP["PSP PIX: MaximaxPay, Mercado Pago"]
  PSP -->|"callback, termina no Dracma"| D
  D -->|"webhook assinado"| CD["Caddy da bet: allowlist de IP do Dracma"]
  CD -->|"POST /api/payments/dracma/webhook"| C
  C -->|"fetch-back GET /v1/deposits/id, /v1/withdrawals/id"| D
Fronteira O que cruza O que nunca cruza
Jogador → portal sessão, valor, método, chave PIX de saque, CPF nada do Dracma
Portal → nero_payments X-Nero-Identity (quem é o jogador), corpo do pedido identidade vinda do corpo
nero_payments → Dracma external_id, player.external_ref (opaco), documento, nome, e-mail/IP/device (risco e exigências), valor em menor unidade, destino do saque, review_requested senha, sessão, token da bet, dados de jogo
Dracma → nero_payments objetos Deposito/Saque, BR Code, eventos assinados, reconciliação credencial de PSP, dados de outra operação

2. Depósito

sequenceDiagram
  autonumber
  participant J as Jogador (front)
  participant P as nero_portal
  participant C as nero_payments
  participant O as outbox dracma_intents
  participant D as Dracma
  participant S as PSP PIX
  J->>P: POST /api/payments/deposits {amount: 50.00, method: pix}
  P->>C: repassa + X-Nero-Identity
  C->>O: grava intent (external_id = dep_uuidv7, valor em centavos, player_ref) na MESMA transação
  C->>D: POST /v1/deposits (Idempotency-Key = external_id, timeout 10 s)
  D->>S: cria a cobrança PIX
  D-->>C: 201 pending + instructions.pix (ou 202 created)
  C-->>J: 201 {deposit_id, status: pending, pix.copy_paste, expires_at}
  J->>S: paga o PIX no app do banco
  S-->>D: confirmação do pagamento
  D->>D: confere valor e pagador (P1 a P4) e vai a paid
  D->>C: webhook deposit.paid (assinado)
  C->>C: verifica assinatura, janela, dedup por Dracma-Event-Id
  C->>D: GET /v1/deposits/dep_... (fetch-back)
  D-->>C: status paid, amount, external_id, player_ref
  C->>C: confere com o intent e credita UMA vez (idempotente por external_id)
  C-->>D: 200
  J->>P: GET /api/payments/deposits/dep_... (polling)
  P->>C: repassa
  C-->>J: status paid

Pontos que o diagrama esconde:

  • Se o POST /v1/deposits der timeout, 5xx ou 429, o intent continua no outbox e um worker repete com a mesma chave. O front recebe 202 {status: creating} e faz polling.
  • Se o webhook se perder, o job de /events (a cada 5 min) encontra o deposit.paid e passa pelo mesmo caminho (dedup → fetch-back → crédito).
  • O polling do front lê o estado local do nero_payments; não é um proxy para o Dracma.

3. Saque

sequenceDiagram
  autonumber
  participant J as Jogador (front)
  participant P as nero_portal
  participant C as nero_payments
  participant L as nero_ledger
  participant D as Dracma
  participant S as PSP PIX
  J->>P: POST /api/wallet/withdraw/request {amount, type: pix, pix_key, pix_type}
  P->>C: repassa + X-Nero-Identity + IP do jogador
  C->>L: RESERVA o valor (débito) na mesma transação do intent wdr_uuidv7
  C->>D: POST /v1/withdrawals (Idempotency-Key = external_id, player.ip)
  D->>D: motor de regras: automático, revisão ou bloqueio
  alt bloqueio (422)
    D-->>C: 422 code
    C->>L: devolve a reserva
    C-->>J: 200 {status: false, message} (mensagem genérica)
  else automático ou revisão
    D-->>C: 201 approved ou under_review
    C-->>J: 200 {status: true, withdrawal_id, state: processing ou under_review}
  end
  D->>S: envia o PIX (automático, ou depois da aprovação no painel)
  S-->>D: liquidado (E2E id)
  D->>C: webhook withdrawal.paid
  C->>D: GET /v1/withdrawals/wdr_... (fetch-back)
  D-->>C: status paid, psp_reference
  C->>L: finaliza a reserva (saída definitiva)
  C-->>D: 200
  • A reserva vem antes do POST e só volta em rejected, failed, cancelled (confirmados por fetch-back) ou em 4xx conclusivo. Timeout e 5xx não devolvem reserva.
  • under_review é decidido por revisores da Nero no painel do Dracma (1 ou 2 pessoas). O resultado chega por webhook como approved → processing → paid, ou rejected.

4. O outbox dracma_intents

Toda chamada de dinheiro ao Dracma nasce como uma linha do outbox, gravada antes da chamada:

Coluna sugerida Conteúdo
external_id dep_<uuidv7> ou wdr_<uuidv7>; também é a Idempotency-Key
kind deposit ou withdrawal
player_ref, amount_minor, currency o que foi pedido (é contra isso que o fetch-back confere)
body os bytes exatos do corpo enviado (repetição manda os mesmos bytes)
state pending → sent → acknowledged (resposta conclusiva) / rejected (4xx) / unknown (tentativas esgotadas)
attempts, next_attempt_at, last_trace_id controle de retry e diagnóstico (Dracma-Trace-Id)
dracma_id, dracma_status, dracma_version último estado aplicado (dep_…/wdr_…, versão monotônica)
stateDiagram-v2
  [*] --> pending: gravado na transação do pedido (e da reserva, no saque)
  pending --> acknowledged: 2xx (ou Idempotent-Replayed)
  pending --> rejected: 4xx conclusivo
  pending --> pending: timeout, 5xx, 429, 409 idempotency_in_progress (mesma chave, backoff)
  pending --> unknown: tentativas esgotadas sem resposta conclusiva
  unknown --> acknowledged: GET ?external_id= achou o objeto
  unknown --> needs_decision: GET ?external_id= inconclusivo
  rejected --> [*]
  acknowledged --> [*]
  • Backoff exponencial com jitter (1 s, 2 s, 4 s … até 60 s), respeitando Retry-After.
  • Cada tentativa é uma requisição nova: timestamp, nonce e assinatura novos; mesma chave e mesmos bytes.
  • Um worker que reinicia retoma do outbox com a mesma chave: nunca duplica.
  • Métrica nero_dracma_outbox_pending e alerta com intent pendente há mais de 10 min (OPERACAO do Dracma).

5. O que o Dracma garante e o que é da Nero

Garantia Quem
Não criar duas cobranças/saques para a mesma chave Dracma (idempotência 30 dias) — desde que a Nero repita com a mesma chave
Não pagar para chave PIX de terceiro, decidir automático × revisão Dracma (motor de regras, DICT)
Creditar uma vez, só em paid Nero (fetch-back + idempotência por external_id)
Reservar antes, devolver só em estado conclusivo Nero
Não perder evento os dois: Dracma reentrega por 72 h e mantém /events; a Nero roda o job de 5 min
Provar que os dois livros batem os dois: reconciliação diária com digest assinado