{
  "info": {
    "name": "Sloteflix API v1",
    "description": "Coleção de integração da Sloteflix.\n\n## Configuração\n\nDefina estas variáveis na coleção ou no ambiente:\n\n| Variável | Exemplo |\n|---|---|\n| `base_url` | `https://SEU_HOST/api/v1` |\n| `agent_code` | `minhaoperacao` |\n| `agent_token` | seu token de 32 chars |\n| `agent_secret_key` | sua secret de 32 chars |\n| `game_code` | `vs20olympgate` |\n| `user_id` | `1042` |\n| `seamless_url` | `https://minhaoperacao.com/api/sloteflix_api` |\n| `master_token` | apenas administradores da plataforma |\n\n`agent_basic` e `jwt` são preenchidos automaticamente pelos scripts.\n\n## Ordem sugerida\n\n1. **Autenticação → Obter JWT** — valida credenciais e popula `{{jwt}}`.\n2. **Agents → Dados do agente** — health check.\n3. **Jogos → Códigos ativos** — confirma que o catálogo responde.\n4. **Jogos → Abrir jogo** — o teste que importa.\n5. **Seamless (simulação)** — dispara callbacks contra o SEU endpoint para validar sua implementação.\n\n## Atenção\n\nAs requisições em **Seamless (simulação)** batem no seu próprio servidor, não na Sloteflix. Use-as para verificar latência (< 600 ms) e idempotência.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },

  "variable": [
    { "key": "base_url", "value": "https://SEU_HOST/api/v1", "type": "string" },
    { "key": "agent_code", "value": "minhaoperacao", "type": "string" },
    { "key": "agent_token", "value": "", "type": "string" },
    { "key": "agent_secret_key", "value": "", "type": "string" },
    { "key": "agent_basic", "value": "", "type": "string" },
    { "key": "jwt", "value": "", "type": "string" },
    { "key": "game_code", "value": "vs20olympgate", "type": "string" },
    { "key": "user_id", "value": "1042", "type": "string" },
    { "key": "seamless_url", "value": "https://minhaoperacao.com/api/sloteflix_api", "type": "string" },
    { "key": "master_token", "value": "", "type": "string" },
    { "key": "test_tx_id", "value": "postman-tx-1", "type": "string" },
    { "key": "test_round_id", "value": "postman-round-1", "type": "string" }
  ],

  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Monta o Bearer base64(\"token:secret\") usado pela autenticação e pelos endpoints de clone.",
          "const token  = pm.variables.get('agent_token');",
          "const secret = pm.variables.get('agent_secret_key');",
          "",
          "if (token && secret) {",
          "  const basic = Buffer.from(`${token}:${secret}`).toString('base64');",
          "  pm.collectionVariables.set('agent_basic', basic);",
          "}"
        ]
      }
    }
  ],

  "item": [
    {
      "name": "Autenticação",
      "item": [
        {
          "name": "Obter JWT",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('access_token presente', () => pm.expect(json.access_token).to.be.a('string').and.not.empty);",
                  "pm.test('token_type = bearer', () => pm.expect(json.token_type).to.eql('bearer'));",
                  "",
                  "if (json.access_token) {",
                  "  pm.collectionVariables.set('jwt', json.access_token);",
                  "  console.log('JWT salvo em {{jwt}}');",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Authorization", "value": "Bearer {{agent_basic}}" },
              { "key": "Accept", "value": "application/json" }
            ],
            "url": { "raw": "{{base_url}}/auth/authentication", "host": ["{{base_url}}"], "path": ["auth", "authentication"] },
            "description": "Troca `base64(\"{{agent_token}}:{{agent_secret_key}}\")` por um JWT.\n\nO Bearer é montado automaticamente pelo script de pré-requisição da coleção.\n\n**Erros:** `INVALID_TOKEN` (header ausente ou fora do formato), `INVALID_AGENT` (par não corresponde a nenhum agente)."
          }
        }
      ]
    },

    {
      "name": "Jogos",
      "item": [
        {
          "name": "Abrir jogo",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('game_url presente', () => pm.expect(json.game_url).to.be.a('string').and.not.empty);",
                  "",
                  "// Qualquer resposta sem game_url é falha, independente do formato do erro.",
                  "if (!json.game_url) {",
                  "  console.error('Launch falhou:', JSON.stringify(json));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [{ "key": "Accept", "value": "application/json" }],
            "url": {
              "raw": "{{base_url}}/games/game_launch?agent_code={{agent_code}}&agent_token={{agent_token}}&game_id={{game_code}}&type=CHARGED&currency=BRL&lang=BR&demo_agent=0&user_id={{user_id}}",
              "host": ["{{base_url}}"],
              "path": ["games", "game_launch"],
              "query": [
                { "key": "agent_code", "value": "{{agent_code}}" },
                { "key": "agent_token", "value": "{{agent_token}}" },
                { "key": "game_id", "value": "{{game_code}}", "description": "O game_code do catálogo — não o id numérico." },
                { "key": "type", "value": "CHARGED", "description": "CHARGED = dinheiro real." },
                { "key": "currency", "value": "BRL" },
                { "key": "lang", "value": "BR" },
                { "key": "demo_agent", "value": "0", "description": "Obrigatório mesmo valendo 0." },
                { "key": "user_id", "value": "{{user_id}}", "description": "Não pode ser vazio nem 0." },
                { "key": "force_demo", "value": "", "description": "Opcional. Força o caminho demo.", "disabled": true },
                { "key": "rtp", "value": "", "description": "Opcional. RTP alvo.", "disabled": true }
              ]
            },
            "description": "Retorna a URL pronta para iframe ou redirect.\n\n**Chame sempre do backend** — a query carrega o `agent_token`.\n\nRequer que o jogador exista na sua base: este launch dispara o callback `account_details` no seu endpoint seamless antes de devolver a URL."
          }
        },
        {
          "name": "Códigos ativos",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.test('status = true', () => pm.expect(json.status).to.be.true);",
                  "pm.test('games é array', () => pm.expect(json.games).to.be.an('array'));",
                  "console.log(`${json.games?.length ?? 0} jogos ativos`);"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [{ "key": "Accept", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/games/games-status", "host": ["{{base_url}}"], "path": ["games", "games-status"] },
            "description": "Só os `game_code` ativos. Resposta leve — use para reconciliação frequente do catálogo."
          }
        },
        {
          "name": "Catálogo completo",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.test('games é array', () => pm.expect(json.games).to.be.an('array'));",
                  "console.log(`${json.games?.length ?? 0} jogos · ${(pm.response.size().body / 1048576).toFixed(2)} MB`);"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [{ "key": "Accept", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/games/all", "host": ["{{base_url}}"], "path": ["games", "all"] },
            "description": "Catálogo inteiro com metadados.\n\n**Sem paginação** — pode passar de vários MB. Rode em job de background, nunca no caminho de uma requisição de usuário.\n\nFaça upsert por `game_code`, nunca por `id`. Jogos que saírem da lista devem ser marcados inativos, não deletados."
          }
        },
        {
          "name": "Hidratar códigos",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"games\": [\"{{game_code}}\"]\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/games/to-add", "host": ["{{base_url}}"], "path": ["games", "to-add"] },
            "description": "Devolve os registros completos dos códigos informados que estão ativos.\n\n**Atenção:** o campo `games` é lido sem validação. Corpo sem esse campo, ou com valor que não seja array, gera HTTP 500."
          }
        }
      ]
    },

    {
      "name": "Provedores",
      "item": [
        {
          "name": "Listar provedores",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.test('providers é array', () => pm.expect(json.providers).to.be.an('array'));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              { "key": "Authorization", "value": "Bearer {{jwt}}" },
              { "key": "Accept", "value": "application/json" }
            ],
            "url": { "raw": "{{base_url}}/games/provider", "host": ["{{base_url}}"], "path": ["games", "provider"] },
            "description": "Provedores ativos. **Requer JWT** — rode *Autenticação → Obter JWT* primeiro."
          }
        }
      ]
    },

    {
      "name": "Clone",
      "item": [
        {
          "name": "Consultar flag de clone",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Authorization", "value": "Bearer {{agent_basic}}" },
              { "key": "Accept", "value": "application/json" }
            ],
            "url": { "raw": "{{base_url}}/games/get-clone-status", "host": ["{{base_url}}"], "path": ["games", "get-clone-status"] },
            "description": "Retorna `is_cloned` do agente autenticado pelo Bearer base64(token:secret)."
          }
        },
        {
          "name": "Inverter flag de clone",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Authorization", "value": "Bearer {{agent_basic}}" },
              { "key": "Accept", "value": "application/json" }
            ],
            "url": { "raw": "{{base_url}}/games/change-clone-status", "host": ["{{base_url}}"], "path": ["games", "change-clone-status"] },
            "description": "**Toggle, não set.** Inverte o valor atual.\n\nUm retry cego reverte a mudança. Para definir um valor explícito use *Agents → Definir/inverter status*, que aceita o campo `status`."
          }
        },
        {
          "name": "Listar jogos clone",
          "request": {
            "method": "GET",
            "header": [{ "key": "Accept", "value": "application/json" }],
            "url": { "raw": "{{base_url}}/games/list-clone", "host": ["{{base_url}}"], "path": ["games", "list-clone"] },
            "description": "Jogos servidos pela trilha de clone. O formato acompanha a resposta do distribuidor."
          }
        }
      ]
    },

    {
      "name": "Free spins",
      "item": [
        {
          "name": "Conceder free spins",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"user_id\": \"{{user_id}}\",\n  \"game_id\": \"{{game_code}}\",\n  \"betlevel\": 1,\n  \"freespincount\": 20,\n  \"expiretime\": \"2026-12-31 23:59:59\",\n  \"freespinID\": \"promo-postman-001\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/games/freespin", "host": ["{{base_url}}"], "path": ["games", "freespin"] },
            "description": "Concede rodadas grátis. Disponível para jogos com `has_freespins = 1`.\n\nGuarde o `freespinID` — é o que permite cancelar depois.\n\nOs prêmios chegam como callback `transaction` com `type: \"win\"`, igual a um prêmio normal."
          }
        },
        {
          "name": "Cancelar free spins",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"user_id\": \"{{user_id}}\",\n  \"game_id\": \"{{game_code}}\",\n  \"freespinID\": \"promo-postman-001\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/games/cancelfreespin", "host": ["{{base_url}}"], "path": ["games", "cancelfreespin"] },
            "description": "Cancela a concessão. Use o mesmo `freespinID` da criação."
          }
        }
      ]
    },

    {
      "name": "Agents",
      "item": [
        {
          "name": "Dados do agente",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.test('id presente', () => pm.expect(json.id).to.be.a('number'));",
                  "pm.test('agent_code confere', () => pm.expect(json.agent_code).to.eql(pm.variables.get('agent_code')));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/agents/get-agent-data", "host": ["{{base_url}}"], "path": ["agents", "get-agent-data"] },
            "description": "O único endpoint de agentes que você chama com as **suas** credenciais. Use como health check."
          }
        },
        {
          "name": "Criar agente (admin)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{master_token}}\",\n  \"email\": \"operador@example.com\",\n  \"password\": \"trocar-depois\",\n  \"subdomain\": \"novaoperacao\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/agents/create", "host": ["{{base_url}}"], "path": ["agents", "create"] },
            "description": "**Requer token administrativo mestre** — operadores integrando a Sloteflix não recebem esse token.\n\nO `endpoint` gerado é um placeholder (`https://{subdomain}.lumosgaming.com`). Ajustá-lo para a URL real é o primeiro passo depois da criação: sem isso nenhum callback seamless chega ao lugar certo."
          }
        },
        {
          "name": "Relatório GGR (admin)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// 404 aqui significa 'zero transações no período', não erro.",
                  "pm.test('200 ou 404', () => pm.expect([200, 404]).to.include(pm.response.code));",
                  "",
                  "if (pm.response.code === 200) {",
                  "  const json = pm.response.json();",
                  "  pm.test('data é array', () => pm.expect(json.data).to.be.an('array'));",
                  "  (json.data ?? []).forEach(r => {",
                  "    const ggr = Number(r.total_bet) - Number(r.total_win);",
                  "    console.log(`${r.provider}: bet ${r.total_bet} · win ${r.total_win} · GGR ${ggr.toFixed(2)}`);",
                  "  });",
                  "} else {",
                  "  console.log('Sem movimento no período (HTTP 404).');",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{master_token}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"start_date\": \"2026-07-01\",\n  \"end_date\": \"2026-07-31\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/agents/consult", "host": ["{{base_url}}"], "path": ["agents", "consult"] },
            "description": "Totais de aposta e prêmio por provedor no intervalo. Base da reconciliação diária.\n\n**Período sem movimento devolve HTTP 404**, não 200 com array vazio."
          }
        },
        {
          "name": "Consultar status (admin)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{master_token}}\",\n  \"agent_code\": \"{{agent_code}}\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/agents/get-status", "host": ["{{base_url}}"], "path": ["agents", "get-status"] },
            "description": "O campo `status` da resposta reflete `is_cloned` — **não** o `agent_status` (ativo/inativo)."
          }
        },
        {
          "name": "Definir/inverter status (admin)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"token\": \"{{master_token}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"status\": true\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{base_url}}/agents/toggle-status", "host": ["{{base_url}}"], "path": ["agents", "toggle-status"] },
            "description": "Com `status` presente, define o valor. **Sem `status`, inverte o atual.**\n\nEm automação sempre envie `status` explicitamente — senão um retry desfaz a mudança."
          }
        }
      ]
    },

    {
      "name": "Seamless (simulação)",
      "description": "Estas requisições batem no SEU endpoint, não na Sloteflix. Use para validar a sua implementação antes do go-live.\n\nCada uma verifica: HTTP 200, corpo JSON, `status` numérico, `balance` presente, e latência abaixo de 600 ms.",
      "item": [
        {
          "name": "1 · account_details",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200 (sempre)', () => pm.response.to.have.status(200));",
                  "pm.test('latência < 600 ms', () => pm.expect(pm.response.responseTime).to.be.below(600));",
                  "",
                  "let json;",
                  "pm.test('corpo é JSON', () => { json = pm.response.json(); });",
                  "if (!json) return;",
                  "",
                  "pm.test('status = 1', () => pm.expect(json.status).to.eql(1));",
                  "pm.test('balance é numérico', () => pm.expect(json.balance).to.be.a('number'));",
                  "",
                  "console.log(`saldo inicial: ${json.balance} · ${pm.response.responseTime} ms`);",
                  "if (typeof json.balance === 'number') pm.collectionVariables.set('_balance_before', json.balance);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"account_details\",\n  \"game\": \"{{game_code}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"user_id\": \"{{user_id}}\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "Primeiro callback do ciclo de vida — dispara na abertura da sessão. Se falhar, o jogo não abre.\n\nÉ o ponto certo para aplicar regras de bloqueio: devolva `status: 11` (bloqueado) ou `status: 10` (limites) e o jogador nunca chega a apostar."
          }
        },
        {
          "name": "2 · user_balance",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "pm.test('latência < 600 ms', () => pm.expect(pm.response.responseTime).to.be.below(600));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('status = 1', () => pm.expect(json.status).to.eql(1));",
                  "pm.test('balance é numérico', () => pm.expect(json.balance).to.be.a('number'));",
                  "",
                  "// É o callback mais frequente da integração — otimize este caminho.",
                  "console.log(`saldo: ${json.balance} · ${pm.response.responseTime} ms`);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"user_balance\",\n  \"game\": \"{{game_code}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"user_id\": \"{{user_id}}\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "O callback mais frequente — o jogo consulta saldo a cada atualização de tela.\n\nResposta sem `balance` é convertida em `OP_31 Insufficient funds`: o jogador vê 'saldo insuficiente' mesmo tendo dinheiro."
          }
        },
        {
          "name": "3 · transaction · aposta",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "pm.test('latência < 600 ms', () => pm.expect(pm.response.responseTime).to.be.below(600));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('status = 1', () => pm.expect(json.status).to.eql(1));",
                  "pm.test('balance é numérico', () => pm.expect(json.balance).to.be.a('number'));",
                  "",
                  "const before = Number(pm.collectionVariables.get('_balance_before'));",
                  "if (!Number.isNaN(before) && typeof json.balance === 'number') {",
                  "  pm.test('debitou exatamente 1.50', () =>",
                  "    pm.expect(Number((before - json.balance).toFixed(2))).to.eql(1.50));",
                  "}",
                  "",
                  "pm.collectionVariables.set('_balance_after_bet', json.balance);",
                  "console.log(`saldo após aposta: ${json.balance} · ${pm.response.responseTime} ms`);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"transaction\",\n  \"type\": \"bet\",\n  \"session_id\": \"postman-session\",\n  \"game\": \"{{game_code}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"round_id\": \"{{test_round_id}}\",\n  \"transaction_id\": \"{{test_tx_id}}\",\n  \"bet\": 1.50,\n  \"win\": 0,\n  \"user_id\": \"{{user_id}}\",\n  \"distributor\": \"timeless\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "Débito de aposta.\n\nDeve ser **atômico** (lock de linha na carteira) e **idempotente** (índice único em `transaction_id`).\n\nValores são decimais na moeda do jogador — `1.50` é R$ 1,50, não R$ 150,00."
          }
        },
        {
          "name": "4 · aposta repetida (idempotência)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "// A resposta correta é sucesso, NÃO status 13.",
                  "pm.test('status = 1 (não 13)', () => pm.expect(json.status).to.eql(1));",
                  "",
                  "const afterBet = Number(pm.collectionVariables.get('_balance_after_bet'));",
                  "if (!Number.isNaN(afterBet)) {",
                  "  pm.test('saldo NÃO mudou — não houve débito duplo', () =>",
                  "    pm.expect(Number(json.balance)).to.eql(afterBet));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "2" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"transaction\",\n  \"type\": \"bet\",\n  \"session_id\": \"postman-session\",\n  \"game\": \"{{game_code}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"round_id\": \"{{test_round_id}}\",\n  \"transaction_id\": \"{{test_tx_id}}\",\n  \"bet\": 1.50,\n  \"win\": 0,\n  \"user_id\": \"{{user_id}}\",\n  \"distributor\": \"timeless\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "**O teste mais importante da coleção.** Mesmo `transaction_id` da requisição anterior, simulando a 2ª tentativa da Sloteflix.\n\nEsperado: `status: 1` com o **mesmo** saldo — sem débito duplo.\n\nNão devolva `status: 13` aqui: o provedor trata como falha e a rodada pode ser cancelada mesmo já tendo sido debitada."
          }
        },
        {
          "name": "5 · transaction · prêmio",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "pm.test('latência < 600 ms', () => pm.expect(pm.response.responseTime).to.be.below(600));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('status = 1', () => pm.expect(json.status).to.eql(1));",
                  "",
                  "// Crédito nunca deve ser rejeitado por saldo insuficiente.",
                  "pm.test('não rejeitado por saldo (status ≠ 0)', () => pm.expect(json.status).to.not.eql(0));",
                  "",
                  "const afterBet = Number(pm.collectionVariables.get('_balance_after_bet'));",
                  "if (!Number.isNaN(afterBet)) {",
                  "  pm.test('creditou exatamente 45.00', () =>",
                  "    pm.expect(Number((Number(json.balance) - afterBet).toFixed(2))).to.eql(45.00));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"transaction\",\n  \"type\": \"win\",\n  \"session_id\": \"postman-session\",\n  \"game\": \"{{game_code}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"transaction_id\": \"{{test_tx_id}}-win\",\n  \"transaction_id_old\": \"{{test_round_id}}\",\n  \"round_id\": \"{{test_round_id}}\",\n  \"bet\": 0,\n  \"win\": 45.00,\n  \"user_id\": \"{{user_id}}\",\n  \"distributor\": \"timeless\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "Crédito de prêmio.\n\n**Nunca rejeite por saldo insuficiente** — crédito só aumenta saldo. Se você responder erro aqui, o jogador ganhou e não recebeu.\n\nNote que o `transaction_id` do prêmio é diferente do da aposta; `transaction_id_old` aponta para a rodada de origem."
          }
        },
        {
          "name": "6 · refund",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('status = 1', () => pm.expect(json.status).to.eql(1));",
                  "pm.test('balance é numérico', () => pm.expect(json.balance).to.be.a('number'));",
                  "",
                  "pm.collectionVariables.set('_balance_after_refund', json.balance);",
                  "console.log(`saldo após estorno: ${json.balance}`);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"refund\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"transaction_id\": \"{{test_tx_id}}\",\n  \"user_id\": \"{{user_id}}\",\n  \"distributor\": \"timeless\",\n  \"balance\": true\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "Estorna a aposta apontada por `transaction_id` — que é o ID da transação **original**, não um ID novo.\n\n**O payload varia por distribuidor:** alguns não enviam `user_id` nem `balance`. Sua implementação precisa achar o jogador a partir do `transaction_id`.\n\nRemova `user_id` e `balance` do corpo e rode de novo para testar esse caminho."
          }
        },
        {
          "name": "7 · refund repetido (idempotência)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "pm.test('status = 1 mesmo já estornado', () => pm.expect(json.status).to.eql(1));",
                  "",
                  "const after = Number(pm.collectionVariables.get('_balance_after_refund'));",
                  "if (!Number.isNaN(after)) {",
                  "  pm.test('saldo NÃO mudou — não houve estorno duplo', () =>",
                  "    pm.expect(Number(json.balance)).to.eql(after));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "2" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"refund\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"transaction_id\": \"{{test_tx_id}}\",\n  \"user_id\": \"{{user_id}}\",\n  \"distributor\": \"timeless\",\n  \"balance\": true\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "Mesmo estorno de novo. Esperado: `status: 1` com o mesmo saldo — estorno já efetivado devolve sucesso, não erro."
          }
        },
        {
          "name": "8 · rollback",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "// O agregador exige estritamente status 1 aqui.",
                  "pm.test('status = 1 (exigido estritamente)', () => pm.expect(json.status).to.eql(1));",
                  "pm.test('balance é numérico', () => pm.expect(json.balance).to.be.a('number'));"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"rollback\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"{{agent_token}}\",\n  \"agent_secret_key\": \"{{agent_secret_key}}\",\n  \"user_id\": \"{{user_id}}\",\n  \"transaction_id\": \"{{test_tx_id}}-win\",\n  \"amount\": 45.00,\n  \"distributor\": \"betby\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "O inverso do refund: **retira** do jogador um valor já creditado.\n\nNote o campo `amount` — não `bet`/`win`.\n\nO agregador exige estritamente `status: 1`. Decida a política de saldo negativo antes do go-live, mas responda `1` em todos os casos, senão o agregador reprocessa."
          }
        },
        {
          "name": "9 · credencial inválida (segurança)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('HTTP 200', () => pm.response.to.have.status(200));",
                  "",
                  "const json = pm.response.json();",
                  "",
                  "// Credencial errada NUNCA pode debitar.",
                  "pm.test('rejeitado (status ≠ 1)', () => pm.expect(json.status).to.not.eql(1));",
                  "pm.test('idealmente status 7 (Invalid token)', () => pm.expect(json.status).to.eql(7));",
                  "",
                  "console.log('Se este teste falhar com status 1, sua carteira aceita credencial forjada. Corrija antes do go-live.');"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/json" },
              { "key": "Accept", "value": "application/json" },
              { "key": "X-Attempt", "value": "1" }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"method\": \"transaction\",\n  \"type\": \"bet\",\n  \"session_id\": \"postman-session\",\n  \"game\": \"{{game_code}}\",\n  \"agent_code\": \"{{agent_code}}\",\n  \"agent_token\": \"token-forjado-invalido\",\n  \"agent_secret_key\": \"secret-forjada-invalida\",\n  \"round_id\": \"attack-round\",\n  \"transaction_id\": \"attack-tx-1\",\n  \"bet\": 999.00,\n  \"win\": 0,\n  \"user_id\": \"{{user_id}}\",\n  \"distributor\": \"timeless\"\n}",
              "options": { "raw": { "language": "json" } }
            },
            "url": { "raw": "{{seamless_url}}", "host": ["{{seamless_url}}"] },
            "description": "**Teste de segurança obrigatório.** Simula alguém que descobriu a URL do seu seamless e tenta debitar com credenciais forjadas.\n\nSeu handler tem que responder `status: 7` (Invalid token) e **não** debitar nada.\n\nA autenticação do seamless é só o trio `agent_code` + `agent_token` + `agent_secret_key` no corpo — não há HMAC. Se você não validar os três, qualquer um drena a sua carteira."
          }
        }
      ]
    }
  ]
}
