Primeiro depósito e primeiro saque no sandbox

Roteiro para a Nero ver o ciclo inteiro funcionando na operação nero-br-hml (ambiente sandbox) antes de escrever o conector: cada passo é um comando copiável, com o resultado esperado. Nenhum dinheiro real se move no sandbox (o PIX é sintético, com o marcador SANDBOX).

Os comandos usam o cliente shell de ../exemplos/shell/ (bash + openssl + curl) e python3 só para ler JSON. O shell serve para depurar: em produção use Go/Node (comparação em tempo constante, segredo fora da linha de comando).

0. Pré-requisitos

  • Credenciais de sandbox recebidas no onboarding: key_test_… com escopos deposits:write, withdrawals:write e read; o segredo num arquivo 0400; o segredo de webhook (whsec_…).
  • IP de saída da máquina cadastrado na allowlist da operação de sandbox (ou allowlist vazia, que no sandbox aceita qualquer IP).
  • URL de webhook cadastrada na operação (ex.: a rota de hml da Nero). Sem ela, os passos que olham o webhook mostram só o log de entregas.
  • Relógio sincronizado (NTP): a janela é de ±300 s.
# Na raiz deste repositório (dracma_docs). Ajuste a base e os caminhos ao que o onboarding entregou.
export DRACMA_BASE_URL="https://sandbox.dracma.example/v1"
export DRACMA_KEY_ID="key_test_SEU_ID"
export DRACMA_KEY_SECRET_FILE="/caminho/seguro/dracma-sandbox.key"      # 0400, fora de qualquer repositório

# dr: chamada assinada; imprime os headers da resposta em stderr e o corpo em stdout.
dr() { bash docs/exemplos/shell/assinar.sh "$@" | awk 'c{print;next} /^\r?$/{c=1;next} {print > "/dev/stderr"}'; }
# js: lê um campo do JSON do stdin. Ex.: js '["id"]'
js() { python3 -c 'import json,sys; v=json.load(sys.stdin); print(eval("v"+sys.argv[1]))' "$1"; }
# novo_id: UUID para external_id e Idempotency-Key (em produção use UUIDv7)
novo_id() { python3 -c 'import uuid; print(uuid.uuid4())'; }
# T: pasta de trabalho temporária para os corpos e respostas
T="$(mktemp -d)"

Para ver a assinatura sem chamar a rede, prefixe qualquer chamada com SO_HEADERS=1 (ex.: SO_HEADERS=1 bash docs/exemplos/shell/assinar.sh GET /ping).

1. Ping: a chave caiu na operação certa?

dr GET /ping

Esperado: HTTP/… 200 e {"ok":true,"operation_id":"<uuid>","environment":"sandbox","server_time":"…"}. Compare server_time com date -u: diferença acima de 5 s = corrija o NTP.

Se vier Causa provável
401 unknown_key key id mal formado, key_test_ contra produção, ou chave revogada (este último só com o segredo certo)
401 invalid_signature segredo errado, key id que não existe, path sem /v1, nonce maiúsculo (sem oráculo: chave inexistente e IP fora da allowlist com assinatura inválida também caem aqui)
401 timestamp_out_of_window relógio
403 ip_not_allowed IP de saída fora da allowlist (só aparece com chave e segredo certos)

2. Moedas, métodos e exigências

dr GET /currencies

Esperado: BRL com methods: ["pix"] e method_details[0].deposit_requirements. Com ["player.name", "player.email"], o PSP de depósito da operação é o Mercado Pago: os dois são obrigatórios (e o e-mail, no sandbox, de domínio .test/.invalid/.example). Com ["player.name"], é a MaximaxPay; com [], o simulador.

3. Primeiro depósito

DEP_EXT="dep_$(novo_id)"
cat > "$T"/nero-dep.json <<EOF
{"external_id":"$DEP_EXT","player":{"external_ref":"nero-hml-jogador-0001","document":{"type":"CPF","number":"52998224725","country":"BR"},"name":"Jogador Teste","email":"jogador1@nero.test","ip":"203.0.113.10"},"amount":{"amount":"5000","currency":"BRL"},"method":"pix"}
EOF
dr POST /deposits "$T"/nero-dep.json "$DEP_EXT" | tee "$T"/nero-dep-resp.json
DEP_ID=$(js '["id"]' < "$T"/nero-dep-resp.json); echo "$DEP_ID"
  • 52998224725 é um CPF de teste (dígito verificador válido, sem titular real). Use CPFs gerados; nunca o de uma pessoa.
  • Esperado: 201 com "status":"pending" e instructions.pix.copy_paste contendo SANDBOX (ou 202 com "status":"created": espere alguns segundos e faça o passo 5).
  • Se voltar 400 com "reason":"required_by_operation" ou "reserved_domain_required": veja o passo 2.

Idempotência na prática — repita exatamente o mesmo comando (mesmo arquivo, mesma chave):

dr POST /deposits "$T"/nero-dep.json "$DEP_EXT"

Esperado: a mesma resposta, com o header Idempotent-Replayed: true. Nenhum depósito novo.

4. Pagar no sandbox

dr POST "/sandbox/deposits/$DEP_ID/pay" "" "sbx_$(novo_id)"

Corpo vazio = pago pelo próprio jogador, valor exato. Resposta 200 com o objeto; o paid chega em seguida. Para testar os caminhos de revisão, mande um corpo:

echo '{"payer_document":{"type":"CPF","number":"11144477735","country":"BR"}}' > "$T"/pagador.json   # outro CPF de teste
# dr POST "/sandbox/deposits/$DEP_ID/pay" "$T"/pagador.json "sbx_$(novo_id)"   → under_review, PAYER_MISMATCH

5. Fetch-back: a única fonte de verdade

dr GET "/deposits/$DEP_ID" | tee "$T"/nero-dep-now.json | js '["status"], v["version"], v["amount"]'

Esperado: ('paid', 3, {'amount': '5000', 'currency': 'BRL'}) (a versão pode variar). É aqui, e só aqui, que o conector credita — depois de conferir external_id, amount, currency e player_ref com o que gravou.

6. Eventos e webhook

dr GET "/events?object_id=$DEP_ID"          # deposit.pending, deposit.paid, na ordem
dr GET /sandbox/webhooks                     # log de entregas: evento, tentativa, status HTTP que a Nero devolveu

Teste o dedup da Nero reentregando o deposit.paid:

EVT=$(dr GET "/events?object_id=$DEP_ID" | js '["data"][-1]["id"]')
echo "{\"event_id\":\"$EVT\"}" > "$T"/redeliver.json
dr POST /sandbox/webhooks/redeliver "$T"/redeliver.json "sbx_$(novo_id)"

Esperado no receptor da Nero: mesmo Dracma-Event-Id, resposta 200, nenhum crédito novo.

Para conferir à mão um webhook capturado (corpo cru em arquivo + os dois headers):

DRACMA_WEBHOOK_SECRETS_FILE=/caminho/seguro/dracma-webhook.keys \
  bash docs/exemplos/shell/verificar-webhook.sh corpo.json "$DRACMA_TIMESTAMP" "$DRACMA_SIGNATURE"

7. Primeiro saque

O primeiro saque de um jogador sempre vai para revisão (FIRST_WITHDRAWAL). No conector, a reserva no nero_ledger acontece antes deste POST.

WDR_EXT="wdr_$(novo_id)"
cat > "$T"/nero-wdr.json <<EOF
{"external_id":"$WDR_EXT","player":{"external_ref":"nero-hml-jogador-0001","document":{"type":"CPF","number":"52998224725","country":"BR"},"name":"Jogador Teste","ip":"203.0.113.10"},"amount":{"amount":"2000","currency":"BRL"},"method":"pix","destination":{"pix":{"key_type":"cpf","key":"52998224725"}},"review_requested":false}
EOF
dr POST /withdrawals "$T"/nero-wdr.json "$WDR_EXT" | tee "$T"/nero-wdr-resp.json
WDR_ID=$(js '["id"]' < "$T"/nero-wdr-resp.json)
js '["status"], v["decision"]["mode"], v["decision"]["reasons"]' < "$T"/nero-wdr-resp.json

Esperado: 201 e ('under_review', 'manual', ['FIRST_WITHDRAWAL']). Aprove (simula o revisor no painel) e pague:

dr POST "/sandbox/withdrawals/$WDR_ID/approve" "" "sbx_$(novo_id)"    # → approved (o Dracma já despacha)
dr POST "/sandbox/withdrawals/$WDR_ID/pay"     "" "sbx_$(novo_id)"    # → 200 com o estado atual; o paid chega depois
sleep 5
dr GET "/withdrawals/$WDR_ID" | js '["status"], v["psp_reference"]'

Esperado: ('paid', '<E2E id>'). No conector: finalizar a reserva só ao ler paid no fetch-back.

8. Segundo saque: o caminho automático

Com o primeiro saque pago e X = 1, um saque que caiba em depositado − sacado (aqui, até R$ 30,00) tende a sair no automático:

WDR2_EXT="wdr_$(novo_id)"
sed "s/$WDR_EXT/$WDR2_EXT/" "$T"/nero-wdr.json > "$T"/nero-wdr2.json
dr POST /withdrawals "$T"/nero-wdr2.json "$WDR2_EXT" | js '["status"], v["decision"]["mode"], v["decision"]["reasons"]'

Esperado: ('approved', 'automatic', []). Se vier under_review, leia decision.reasons: as regras da operação (GET /v1/rules) podem pedir revisão por outros motivos (janela, chave nova, horário). Sem player.ip o resultado é sempre under_review com OWNERSHIP_UNVERIFIED.

Para ver a devolução da reserva: crie outro saque e use .../reject (de under_review) ou .../fail (de approved/processing), e confira rejected/failed no fetch-back.

9. Queda simulada e retomada sem duplicar

echo '{"down":true}'  > "$T"/down.json
echo '{"down":false}' > "$T"/up.json
dr POST /sandbox/outage "$T"/down.json "sbx_$(novo_id)"

DEP2_EXT="dep_$(novo_id)"
sed "s/$DEP_EXT/$DEP2_EXT/" "$T"/nero-dep.json > "$T"/nero-dep2.json
dr POST /deposits "$T"/nero-dep2.json "$DEP2_EXT"        # → 503 service_unavailable, Retry-After: 5

dr POST /sandbox/outage "$T"/up.json "sbx_$(novo_id)"
dr POST /deposits "$T"/nero-dep2.json "$DEP2_EXT"        # MESMA chave, MESMO corpo → 201/202
dr POST /deposits "$T"/nero-dep2.json "$DEP2_EXT"        # → mesma resposta, Idempotent-Replayed: true
dr GET "/deposits?external_id=$DEP2_EXT" | js '["data"].__len__()'   # → 1

É exatamente o comportamento que o outbox do nero_payments precisa ter em produção.

10. No dia seguinte: reconciliação

ONTEM=$(date -u -d yesterday +%F)
dr GET "/reconciliation?date=$ONTEM&currency=BRL&limit=200"

Esperado (depois das 02:00 UTC): itens deposit/withdrawal com status: paid, totals, digest e o header Dracma-Signature na última página. Antes das 02:00 UTC: 409 day_open com Retry-After. Confira o digest com Digest/VerifyDigest dos exemplos Go ou Node.

Limpeza

rm -rf "$T"