Divergências conhecidas (código × contrato)¶
Esta página lista, sem esconder, onde o código atual do Dracma (dracma_api, dracma_webhooks,
dracma_executor_rs, conferidos em 2026-09-27) faz algo diferente do Contrato v1 1.1.0, do
openapi.yaml ou dos documentos irmãos (REGRAS, SEGURANCA). As outras páginas deste guia descrevem o que o
código faz hoje e apontam para cá.
Como usar:
- "Seu código" diz o que o conector deve fazer para funcionar nos dois mundos (hoje e depois da correção). Quase sempre é seguir o comportamento mais restritivo.
- Cada item tem um id estável (
D-A1,D-D2…). Use-o ao abrir chamado ou ao citar em código. - Quando uma divergência for resolvida, ela sai desta lista no mesmo PR que muda o código ou o contrato.
Autenticação, transporte e erros¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-A1 | §3.6: chave de uma operação com certificado de outra → 401 |
a verificação do certificado mTLS fica na borda (proxy TLS) de produção; o cruzamento chave × certificado não está no código da API | nunca misture chave e certificado de operações diferentes; trate o 401 como possível |
| D-A2 | 5 req/s para /events e /reconciliation (por operação) |
balde de listagem de 5 req/s por chave para GET /deposits, /withdrawals, /events (sem id); /reconciliation usa o balde geral de leitura |
pagine /events e /reconciliation devagar (≤ 5 req/s); fetch-back sempre por id |
| D-A3 | catálogo §7 não tem 413 |
corpo > 64 KiB → 413 com code: validation_failed, reason: too_large |
trate 413 como 4xx de validação |
| D-A4 | nonce = "32 hex" | exige minúsculo (^[0-9a-f]{32}$); maiúsculo → 401 invalid_signature |
gere hex minúsculo |
| D-A5 | §4.3: Dracma-Event-Id ≠ id do corpo → 400 |
SEGURANCA I8 diz 401 (o documento irmão está errado) |
responda 400 (como o operador de referência) |
| D-A6 | não previsto | 20 falhas de assinatura/min do mesmo IP → as seguintes viram 429 rate_limited |
corrija a assinatura; não repita em laço um 401 |
| D-A8 | 503 psp_unavailable / outside_operating_hours |
não emitidos; indisponibilidade sai como 503 service_unavailable |
trate todo 503 igual: repita com a mesma chave depois do Retry-After |
| D-A9 | limites por jogador não constam no contrato | POST /deposits: 10 depósitos criados por jogador por hora; POST /withdrawals: 3 saques pedidos por jogador por hora (SEGURANCA §9.1) → 429 rate_limited com Retry-After |
mostre "tente mais tarde" ao jogador; no saque, mantenha a reserva e repita com a mesma chave depois do Retry-After |
Idempotência e corpo¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-I1 | openapi: operation_id no corpo "é ignorado se existir" |
ignorado só na raiz do corpo; aninhado → 400 unknown_field |
nunca envie operation_id |
| D-I2 | chave "em processamento" | a reserva da chave dura 30 s; depois disso outra execução pode assumir | não faça duas chamadas concorrentes com a mesma chave |
Depósitos¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-D2 | POST /deposits/{id}/cancel cancela o depósito |
muda só o Dracma; não existe comando para cancelar a cobrança no PSP — o PIX continua pagável até expirar | depósito cancelled com paid_amount preenchido: não credite; needs_decision + alerta |
| D-D3 | paid → refunded / paid → chargeback |
gatilho só no sandbox: POST /v1/sandbox/deposits/{id}/refund e POST /v1/sandbox/deposits/{id}/chargeback (rotas aditivas ao openapi do contrato; só de paid, outro estado → 409; emitem deposit.refunded/deposit.chargeback). Em produção ainda sem gatilho (nem PSP, nem evento do executor) |
implemente o tratamento (débito por política, needs_decision) e teste-o no sandbox com essas rotas |
| D-D4 | openapi CriarDeposito tem method_options e fx_quote_id |
os dois → 400 unknown_field |
não envie em PIX |
| D-D5 | catálogo de eventos | não existe deposit.created; o primeiro evento é deposit.pending com object_version 2 |
não espere evento de criação |
| D-D6 | §6.1 não tem transição para pagamento depois de terminal | pagamento em expired/cancelled/failed: estado não muda; paid_amount, paid_at e pagador são gravados e o Dracma alerta |
nunca credite fora de paid; paid_amount em terminal = needs_decision |
| D-D7 | REGRAS §12 (catálogo de motivos) | emite também RULES_UNAVAILABLE e RULES_INVALID_RESPONSE (em review_reasons e decision.reasons) |
leitor tolerante: motivo desconhecido não muda nada |
| D-D8 | Documento.type aceita PASSPORT, NATIONAL_ID, TAX_ID |
em PIX só CPF/CNPJ: os outros → 422 document_invalid (depósito) ou bloqueio H2 (saque) |
em PIX mande só CPF/CNPJ |
Exigências por operação (api-req)¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-R1 | player.email/player.name opcionais; Moeda sem method_details; Problema.errors[].reason sem os motivos novos |
GET /currencies traz method_details[] (method, deposit_requirements, psp_notes) — campo aditivo: está no openapi.yaml servido (schema DetalheMetodo), não no openapi do contrato 1.1.0; POST /deposits recusa com 400 (required_by_operation, format, max_length, reserved_domain_required) antes de criar objeto. Mercado Pago exige player.name + player.email; MaximaxPay exige player.name. player.email é validado sempre que vier; player.name > 120 → 400 (limite que o openapi já tinha e não era aplicado). Commit 6d81813 do dracma_api |
leia as exigências de GET /currencies; mande os campos; trate o 400 como recusa |
| D-R2 | §11 (lado da Nero): corpo do depósito do front é {amount, method, accept_bonus, cpf?} |
a operação com Mercado Pago exige player.name e player.email (MaximaxPay exige player.name) |
o conector preenche nome e e-mail a partir do cadastro do jogador (servidor), nunca do corpo do front |
| D-R3 | exemplos do openapi usam jogador1@test.nerobet.lol |
fora de produção o Dracma só aceita e-mail de TLD reservado (.test, .invalid, .example); test.nerobet.lol recebe reserved_domain_required |
em sandbox use …@….test (ex.: jogador1@nero.test) |
Saques¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-W1 | approved → processing = "enviado ao PSP" |
vira processing no primeiro retorno do PSP; envio recusado antes de sair (caixa, guarda de ambiente) deixa em approved + alerta |
approved pode durar; mantenha a reserva |
| D-W2 | approved → cancelled permitido (cancelar "só antes de processing") |
com o comando de envio já publicado, cancelar → 409 (evita pagar e devolver) |
trate 409 no cancel como "espere o desfecho" |
| D-W3 | diagrama §6.2 não desenha requested → cancelled |
permitido (o texto do §6.2 e o openapi permitem) | — |
| D-W4 | Decisao.mode obrigatório |
em requested (DICT pendente): mode: "manual", reasons: [], decided_at: null |
não conclua nada de mode em requested |
| D-W5 | catálogo de eventos | não existe evento público withdrawal.requested |
— |
| D-W6 | paid → returned quando o banco devolve |
só a ação de sandbox return aplica; o executor ainda não publica devolução real |
implemente a política de returned (D7) mesmo assim |
| D-W7 | ações de sandbox mudam o estado | pay/fail respondem 200 com o estado atual; o desfecho chega por evento |
espere o webhook / faça fetch-back |
| D-W8 | player.ip opcional |
sem player.ip o saque nunca é automático: under_review com OWNERSHIP_UNVERIFIED (o PSP de saque exige o IP do titular; decisão D17 do Dracma) |
mande o IP do jogador em todo saque |
| D-W9 | §2: POST /withdrawals responde 202 se o PSP demorar |
sempre 201 (a criação não espera o PSP) |
trate 201 e 202 igual |
| D-W10 | motor fora → (implícito) erro | na criação do saque: 201 under_review com RULES_UNAVAILABLE (na dúvida, revisão); no depósito: 503 |
mantenha a reserva em under_review |
Reconciliação e leitura¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-C1 | digest: itens ordenados por object_id |
empate (mesmo object_id em dois itens) desempatado por kind crescente |
ordene por (object_id, kind) — os exemplos já fazem |
| D-C2 | — | não há vetor normativo do digest em vetores-assinatura.json |
use os testes dos exemplos; sugestão ao Dracma: publicar um vetor |
| D-C3 | GET /rules sem currency |
usa a primeira moeda habilitada da operação | — |
| D-C4 | pending_in do caixa |
calculado pela API (depósitos recebidos e ainda em revisão), não vem do livro | use só para acompanhamento |
Webhooks¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-H1 | §5: backoff exponencial com jitter | intervalos fixos, sem jitter | não dependa do horário exato da reentrega |
| D-H2 | omisso sobre redirect | 3xx conta como falha (reentrega) |
responda 2xx direto na URL cadastrada |
| D-H3 | lista de IPs de egress publicada e assinada | ainda não há endpoint público da lista; ela chega no onboarding | assinatura + fetch-back são a defesa principal; aplique a lista quando recebê-la |
Sandbox e conformidade¶
| Id | Contrato diz | Código faz hoje | Seu código |
|---|---|---|---|
| D-S1 | o openapi do contrato 1.1.0 não declara Idempotency-Key nas rotas /sandbox/* |
exigida em todo POST; o openapi.yaml publicado (o servido) declara |
mande chave nova em cada ação de sandbox |
| D-S2 | §10: suíte em nero_v2/nero_dracma_sandbox/conformidade/ |
a suíte está em dracma_api/conformance/ e é entregue no onboarding |
— |
| D-S3 | o openapi do contrato 1.1.0 (cópia fiel em contrato-1.1.0.openapi.yaml) traz o server 127.0.0.1:3016 ("Dracma FALSO da bet") e o e-mail jogador1@test.nerobet.lol |
o openapi.yaml publicado é o servido pela API: sem esse server; o exemplo de e-mail continua |
ignore esse server; ver D-R3 para o e-mail |
| D-S4 | allowlist vazia = sem acesso | vale em produção; em sandbox, lista vazia aceita qualquer IP | cadastre a allowlist desde o sandbox |
Divergências já resolvidas (histórico)¶
| Id | O que era | Como ficou |
|---|---|---|
| D-D1 | o executor não publicava charge.paid.v1 quando valor ou pagador divergiam (o depósito ficava pending) |
corrigido no dracma_executor_rs (a892360): o pagamento sempre chega com os valores reais e a API decide under_review |
| D-S0 | a suíte de conformidade chamava /sandbox/* sem Idempotency-Key |
corrigido: a suíte gera idem_<uuidv7> em todo POST |