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 escoposdeposits:write,withdrawals:writeeread; o segredo num arquivo0400; 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:
201com"status":"pending"einstructions.pix.copy_pastecontendoSANDBOX(ou202com"status":"created": espere alguns segundos e faça o passo 5). - Se voltar
400com"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¤cy=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"