Depósito no nero_payments

Implementação de POST /api/payments/deposits e GET /api/payments/deposits/{deposit_id} sobre o POST /v1/deposits do Dracma. Referência completa da API: ../depositos.md.

1. Passo a passo do POST /api/payments/deposits

flowchart TD
  A["Pedido do front + X-Nero-Identity"] --> B{"Corpo válido? amount em reais com até 2 casas, method = pix"}
  B -->|não| B1["400 ao front"]
  B -->|sim| C["Carrega o jogador pelo id da asserção: external_ref, CPF, nome, e-mail, IP"]
  C --> D{"Exigências da operação (GET /v1/currencies em cache) atendidas?"}
  D -->|não| D1["422 ao front: complete o cadastro (sem chamar o Dracma)"]
  D -->|sim| E["Transação: grava intent dep_uuidv7 com os bytes do corpo"]
  E --> F["POST /v1/deposits, Idempotency-Key = external_id, timeout 10 s"]
  F -->|"201 pending"| G["Aplica o estado, devolve 201 com o pix"]
  F -->|"202 created"| H["Devolve 202 status creating"]
  F -->|"4xx"| I["Intent rejected, devolve o erro mapeado"]
  F -->|"timeout, 5xx, 429"| J["Intent segue pending no outbox, devolve 202 creating"]
  1. Identidade. O jogador vem da asserção X-Nero-Identity do portal. Nada de player_id, e-mail ou CPF de outro jogador vindo do corpo.
  2. Valor sem float. O front manda amount em reais (50.00). Converta para centavos por texto, nunca por float * 100:
Entrada (amount) Centavos (amount.amount)
50 "5000"
50.5 "5050"
50.00 "5000"
0.01 "1"
50.001, -1, 0, 1e3, "50,00" recuse com 400
centavos(numero_json_como_texto):
  exigir casa com ^[0-9]{1,12}(\.[0-9]{1,2})?$         # sem sinal, sem expoente, até 2 casas
  inteiro, frac = separar no "."; frac = (frac + "00")[0:2]
  c = remover_zeros_a_esquerda(inteiro + frac); exigir c != "0"
  devolver c                                              # string de inteiro

Em Go, leia o corpo com json.Decoder.UseNumber(); em Node, não use JSON.parse direto no valor (leia o número como texto ou use um parser que preserve o literal). 3. Exigências da operação. Antes de montar o corpo, confira method_details[].deposit_requirements do GET /v1/currencies (em cache; recarregue no boot e a cada hora). Com o Mercado Pago como PSP de depósito, o Dracma exige player.name e player.email; com a MaximaxPay, player.name. Se o jogador não tem o campo, responda ao front pedindo para completar o cadastro, sem chamar o Dracma (economiza um external_id recusado). 4. Documento. cpf do corpo só é aceito se for o CPF do cadastro do jogador (ou o primeiro cadastro dele). O Dracma fixa o documento no primeiro uso: documento diferente depois → 422 document_mismatch. 5. Outbox. Na mesma transação: gere external_id = dep_<uuidv7>, serialize o corpo uma vez e grave os bytes. A Idempotency-Key é o próprio external_id. 6. Chamada. POST /v1/deposits assinado, timeout 10 s. Trate a resposta pela tabela da seção 3.

2. Corpo enviado ao Dracma

{
  "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "player": {
    "external_ref": "0192a3b4-0000-7000-8000-000000000001",
    "document": { "type": "CPF", "number": "12345678909", "country": "BR" },
    "name": "Jogador Teste",
    "email": "jogador1@nero.test",
    "ip": "203.0.113.10"
  },
  "amount": { "amount": "5000", "currency": "BRL" },
  "method": "pix",
  "expires_in_seconds": 1800
}
Campo Origem na Nero Nota
external_id gerado pelo nero_payments (dep_<uuidv7>) = deposit_id do front = Idempotency-Key
player.external_ref id interno imutável do jogador (IAM da Nero) nunca e-mail, telefone ou CPF
player.document CPF do cadastro, só dígitos só CPF/CNPJ em PIX
player.name nome do cadastro exigido nas operações reais (Mercado Pago e MaximaxPay); ≤ 120 caracteres
player.email e-mail cadastrado do jogador só quando a operação exige (ou para risco); em hml, só .test/.invalid/.example
player.ip IP do jogador que o portal recebeu do Caddy ajuda o risco; pode ser exigido por operação
amount centavos da seção 1 string de inteiro
expires_in_seconds opcional (300–86400; default 1800) o prazo efetivo vem em instructions.pix.expires_at
accept_bonus não vai ao Dracma bônus é regra da bet

Nunca envie operation_id, status, version, paid_amount, method_options ou fx_quote_id: o Dracma recusa campo desconhecido com 400 (exceto operation_id na raiz, que ele descarta).

3. Resposta do Dracma → resposta ao front

Dracma Outbox Front
201, status: pending acknowledged; grava dracma_id, version, instructions 201 {deposit_id, status: "pending", amount, currency: "BRL", method: "pix", pix: {copy_paste, qr_code_png_base64?, expires_at}, expires_at} — use o expires_at do Dracma
201, status: failed acknowledged (terminal) 201 {status: "failed"}
202, status: created acknowledged 202 {deposit_id, status: "creating"}; o PIX chega por deposit.pending
400 validation_failed com required_by_operation / reserved_domain_required / format rejected 422 "complete seu cadastro" (e-mail)
outro 400 validation_failed rejected + alerta (é bug do conector) 500 genérico ao front
400 currency_not_enabled / method_not_enabled rejected + alerta 503 "depósito indisponível"
409 duplicate_external_id acknowledged com o objeto de existing como 201/202 conforme o estado de existing
409 idempotency_key_reused needs_decision + alerta (bug) 500 genérico
422 document_invalid / document_mismatch rejected 422 "confira seu CPF"
422 amount_below_minimum / amount_above_maximum rejected 422 com o limite (leia de GET /v1/rules)
422 player_blocked / velocity_exceeded rejected 422 "não foi possível gerar o depósito agora" (genérico, sem motivo)
429 rate_limited (10 depósitos por jogador por hora) pending (repete depois do Retry-After) ou rejected se a Nero preferir desistir 429 "muitas tentativas, aguarde"
503 / 5xx / timeout pending, backoff, mesma chave 202 {status: "creating"}

4xx não cria objeto no Dracma e não se repete. Tentar de novo depois de corrigir o cadastro = novo external_id (novo deposit_id).

4. GET /api/payments/deposits/{deposit_id}

  • Só o dono: deposit_id de outro jogador → 404 (mesma resposta de "não existe").
  • Lê o estado local e traduz pela tabela de estados-no-front.md.
  • Não é proxy do Dracma. Opcional: se o intent está parado em creating/pending há mais de 60 s, faça um GET /v1/deposits/{dracma_id} (no máximo a cada 10 s por depósito). Sem dracma_id (resposta perdida), use GET /v1/deposits?external_id=<deposit_id> — que está no balde de listagem (5 req/s por chave): nunca por polling do front.

5. Crédito

O crédito acontece só no caminho do webhook//events (webhook-e-jobs.md), depois do fetch-back, e é idempotente pelo external_id:

-- no nero_ledger, na mesma transação que marca o evento como visto e o intent como paid
INSERT INTO lancamento (origem, external_id, jogador_id, valor_minor, tipo)
VALUES ('dracma', $1, $2, $3, 'credito_deposito')
ON CONFLICT (origem, external_id, tipo) DO NOTHING;   -- restrição única: 1 crédito por depósito

O valor creditado é o amount_minor do intent (o que a Nero pediu), depois de conferir que o amount.amount do fetch-back é idêntico. Divergência → não credita, needs_decision, alerta (nero_dracma_fetchback_mismatch_total).