# O contrato de README.md expresso em OpenAPI 3.1. # # Destina-se a geração de cliente, importação no Postman e validação de # requisição. O README permanece a fonte sobre a razão de cada regra; este # arquivo especifica apenas a forma. # # ESTE ARQUIVO ESPECIFICA UMA DAS DUAS DIREÇÕES: /wallet/*, implementado pelo # OPERADOR e chamado pelo jogo. # # A outra direção — POST /v1/games e POST /v1/launch, implementadas pelo JOGO e # chamadas pelo operador — está especificada no fim deste arquivo, sob o servidor # `jogo`. Ela existe desde que o lançamento passou a trocar token e player_id por # um ticket, servidor a servidor; o caminho antigo, em que o operador montava a # URL com o token dela dentro, continua funcionando e está documentado como # legado em README.md. openapi: 3.1.0 info: title: Jumbo — integração com o operador version: 1.0.0 description: | Carteira seamless: o jogo não mantém saldo, e a carteira do operador é a única fonte de verdade. Se o operador recusa um débito, a aposta não se concretiza. **Valores monetários são inteiros, em centavos.** `R$ 3,56` é `356`. Nenhum campo monetário admite ponto flutuante — o bundle do jogo já enviou `114.99999999999999` para uma aposta de R$ 1,15 quando o campo era decimal. **Erro de NEGÓCIO é `200` com `status: error` e um `error_code`;** erro de PROTOCOLO é o status que lhe cabe — `401` para assinatura, `400` para corpo malformado. A distinção é o que separa uma decisão sua de uma falha de transporte: `5xx` pode vir de um proxy no caminho que a sua carteira nunca viu, então uma decisão mandada como `5xx` é retentada para sempre. O que nunca pode acontecer é o inverso — `200` sem `status: error` para uma recusa. Um proxy registra sucesso, o jogo credita a resposta como boa, e a retentativa automática deixa de ocorrer. **As duas direções não usam a mesma convenção, e isso é deliberado.** A regra do `200` acima vale para a CARTEIRA (`/balance`, `/bet`, `/win`, `/rollback`), onde uma recusa é uma decisão de negócio sua. As duas rotas do JOGO (`/v1/games`, `/v1/launch`) respondem `4xx`, porque ali não há decisão de negócio: o que pode dar errado é o pedido estar malformado, o token ser fraco ou a assinatura não bater. Os conjuntos de `error_code` também são disjuntos — veja `Erro` para a carteira e `ErroDoJogo` para o jogo. servers: - url: https://casa.example/wallet description: > o operador — implementa /balance, /bet, /win e /rollback - url: https://jumbo.aircrash.net description: > o jogo — implementa /v1/games e /v1/launch # Sem `security` global, de proposito. # # As duas direcoes usam esquemas diferentes: a carteira do operador le # `Authorization: Bearer` E `X-Signature`; as duas rotas do jogo leem SO a # assinatura, e nunca olham o Authorization. Um default global aplicaria o Bearer # aos quatro endpoints e um gerador de cliente mandaria uma credencial que o outro # lado ignora -- e quem depurasse acharia que a chave e que esta errada. tags: - name: carteira description: implementado pelo OPERADOR, chamado pelo jogo - name: jogo description: implementado pelo JOGO, chamado pelo operador paths: /balance: post: tags: [carteira] summary: Quem é esta sessão, e quanto ela tem description: | A porta de entrada: nenhum jogador entra sem uma resposta 200 daqui. O jogo recebe na URL um `sid` que o operador emitiu, e que por si só não diz nada. Esta chamada é a pergunta "de quem é este token?", e o `player_id` da resposta é o que o jogo devolve em todo `bet`, `win` e `rollback` daquele jogador. **Um token desconhecido, expirado ou revogado tem de sair como 401.** Aceitá-lo é deixar qualquer pessoa entrar com um `sid` inventado. Chamado a cada entrada, e não a cada rodada — o saldo depois disso acompanha as respostas de `bet` e `win`. Perguntar sempre é o que faz uma revogação do lado do operador valer na entrada seguinte. É o único endpoint sem `transaction_id`, por não movimentar valores, e o único sem `player_id` no pedido: é ele que o descobre. operationId: saldo security: - chaveApi: [] assinatura: [] requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object required: [token] properties: token: type: string description: o mesmo valor que veio no `?sid=` da URL do jogo example: 0f0e0d0c-0b0a-4009-8008-070605040302 responses: '200': description: 200 com status ok, ou 200 com status error — a sessão é válida content: application/json: schema: oneOf: - type: object required: [player_id, balance, currency] properties: player_id: type: string description: identificador do jogador no operador example: op-user-99321 balance: type: integer description: saldo em centavos example: 100000 currency: type: string example: BRL description: > A moeda da conta. Quando a sessao veio de um POST /v1/launch que declarou moeda, esta TEM de bater com aquela: divergencia faz o jogo recusar a sessao, porque as duas sao afirmacoes do mesmo operador e a moeda decide quais limites de aposta valem. Ausente, vale a do lancamento. No caminho legado (token direto no ?sid=) nao ha moeda declarada, e esta e a unica fonte. player_name: type: string description: | Nome civil ou apelido — o operador decide. O jogo o ofusca antes de exibir: `José da Silva` vira `J***é`. example: José da Silva freebets: type: array description: | Apostas custeadas pelo operador. Cada entrada é uma concessão de `quantity` rodadas valendo `amount` centavos cada. O `id` é a chave da concessão, e repeti-lo é um no-op: nem quando ainda está aberto, nem quando já foi gasto. Reenviar a lista inteira de concessões abertas em todo `/balance` é o uso esperado, e é o que torna o relançamento inofensivo. Reenviar o mesmo `id` com um `amount` diferente também não faz nada — o valor da primeira concessão prevalece. Para mudar o valor, emita um `id` novo. Entrada inválida (`id` vazio, `amount` ausente, zero, negativo ou com fração de centavo) é **descartada em silêncio**: as outras sobrevivem, o jogador entra normalmente, e o brinde simplesmente não existe. Atenção ao pior caso: `5.00`, escrito pensando em reais, vira CINCO CENTAVOS. Só este endpoint concede. Um array `freebets` numa resposta de /bet, /win ou /rollback é lido e descartado. items: type: object required: [id, amount] properties: id: type: string description: único por concessão, nunca reciclado por campanha example: promo-abc quantity: type: integer minimum: 1 maximum: 1000 default: 1 description: | Quantas rodadas. Opcional; ausente vale 1. Fora da faixa, a concessão inteira é descartada. Internamente cada rodada vira uma concessão própria: `quantity: 10` sobre o id `promo-abc` produz as jogadas `promo-abc#1` a `promo-abc#10`, e é esse id sufixado que volta no `freebet_id` do /bet. Sem `quantity`, ou com `quantity: 1`, o id não ganha sufixo. example: 10 amount: type: integer description: | O valor de CADA rodada, em centavos, inteiro e positivo. O total é `quantity × amount`, e não é um campo do corpo. **Tem de caber entre a aposta mínima e a máxima da moeda.** Fora dessa faixa a concessão é descartada na entrada, porque o jogador não conseguiria gastá-la: o jogo recusa o valor antes de consumir o brinde, e ele ficaria anunciado na tela sem nunca funcionar. Os limites vigentes aparecem no card de LIMITE DO JOGO, e variam por moeda. example: 100 - $ref: '#/components/schemas/Erro' '401': description: token desconhecido, expirado ou revogado content: application/json: schema: { $ref: '#/components/schemas/Erro' } '403': { $ref: '#/components/responses/Erro' } /bet: post: tags: [carteira] summary: Debita a aposta description: | **A aposta só existe se a resposta for 200.** `round_id` agrupa a rodada, e um jogador pode manter **até cinco apostas na mesma rodada**, cada uma com seu `transaction_id`. Desduplicação por `round_id` recusaria as quatro últimas. Com `freebet_id` preenchido o operador **não debita** — responde 200 com o saldo inalterado. operationId: apostar security: - chaveApi: [] assinatura: [] requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Movimento' - type: object required: [amount] properties: amount: { $ref: '#/components/schemas/Centavos' } freebet_id: type: [string, 'null'] description: | Preenchido, é um brinde: o operador **não debita** e responde 200 com o saldo inalterado. O `amount` vem cheio assim mesmo, porque é o valor da aposta. O campo existe SEMPRE, valendo `null` na aposta comum: ramifique em `freebet_id !== null`, nunca na presença da chave. O id pode não ser o que a tela do jogador mostrou. Se o brinde pedido já não estiver aberto, o jogo substitui pelo aberto mais antigo de valor exatamente igual — case contra a fila de concessões, não contra uma expectativa. Uma recusa 4xx devolve o brinde à fila: o mesmo id volta num /bet posterior, com outro `transaction_id`. É devolução, não gasto em dobro. responses: '200': description: 200 com status ok, ou 200 com status error — debitado content: application/json: schema: { $ref: '#/components/schemas/Resposta' } '400': { $ref: '#/components/responses/Erro' } '401': { $ref: '#/components/responses/Erro' } '402': description: saldo insuficiente — o jogo recusa a aposta ao jogador content: application/json: schema: { $ref: '#/components/schemas/Erro' } '403': { $ref: '#/components/responses/Erro' } '409': description: | `TOKEN_EXPIRED`, ou `DUPLICATE_TRANSACTION` quando o mesmo `transaction_id` retorna com corpo divergente. Corpo **idêntico** não é erro: constitui retentativa, e a resposta deve ser a mesma, com 200. content: application/json: schema: { $ref: '#/components/schemas/Erro' } /win: post: tags: [carteira] summary: Credita o ganho description: | Chamado **apenas quando há ganho** — rodada perdida não gera chamada, por já ter havido o débito. **Crédito órfão deve ser recusado:** não havendo débito registrado sob aquele `bet_id`, o `win` deve ser recusado com 404. Sem essa verificação, um `win` fabricado credita saldo sem que exista aposta. Token expirado **não** pode barrar este endpoint — o `win` chega depois, por vezes muito depois, e recusá-lo retém valor indevidamente. Um 4xx aqui não recusa nada: o movimento continua na fila e volta com o mesmo `transaction_id` até 20 vezes. operationId: ganhar security: - chaveApi: [] assinatura: [] requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Movimento' - type: object required: [amount, bet_id, type] properties: type: type: string enum: [win, lose] description: | `win` credita; `lose` fecha a aposta sem creditar nada. O `lose` so aparece com a capacidade `zero_win` ligada, que vem DESLIGADA por padrao: sem ela, rodada perdida nao gera chamada nenhuma. Trate o campo como obrigatorio de todo jeito -- ele sempre vem, e ligar a capacidade e mudanca de configuracao nossa, nao de contrato. example: win amount: allOf: - $ref: '#/components/schemas/Centavos' description: | **Não valide `amount == stake × multiplicador`.** É a regra, mas há dois desvios legítimos: para MAIS quando a rodada é premiada (a promoção da casa é somada dentro do mesmo win, e o extrato a separa em `bonusCents`), e para MENOS quando o ganho bate no teto por aposta. Recusar fora da igualdade exata recusa crédito legítimo. Numa aposta de freebet este valor é o **payout completo** (`valor do brinde × multiplicador`), stake incluída. Subtraí-la aqui paga metade num saque de 2,00x. bet_id: type: string description: o `transaction_id` do débito correspondente freebet_id: type: [string, 'null'] description: | Herdado do débito. É o único sinal, no crédito, de que aquele ganho vem de um brinde custeado pelo operador. responses: '200': description: 200 com status ok, ou 200 com status error — creditado content: application/json: schema: { $ref: '#/components/schemas/Resposta' } '400': { $ref: '#/components/responses/Erro' } '401': { $ref: '#/components/responses/Erro' } '404': description: não há débito registrado sob esse `bet_id` content: application/json: schema: { $ref: '#/components/schemas/Erro' } /rollback: post: tags: [carteira] summary: Desfaz um movimento description: | Utilizado em **quatro** situações: cancelamento pelo jogador, recusa posterior ao débito, encerramento com aposta em curso, e aposta órfã encontrada no boot. **O corpo não informa valor** — o operador reverte pelo valor da transação referenciada, e não pelo que o jogo declare. Informar valor permitiria devolver quantia superior à debitada. **Cuidado com o estorno de freebet.** O corpo também não carrega `freebet_id`: olhando só para ele, o estorno de um brinde é indistinguível do de dinheiro real. Quem lembra é o `bet_id` — foi naquele `/bet` que o `freebet_id` veio. Um débito de freebet não moveu saldo nenhum, então revertê-lo também não pode mover: creditar "o valor da aposta referenciada" devolve uma stake que nunca foi cobrada, e o jogo adota esse saldo inflado como verdade. **Transação inexistente responde 200.** Não havendo o que desfazer, o estado desejado — a ausência daquele movimento — já se verifica. Responder erro levaria o jogo a retentar indefinidamente. operationId: estornar security: - chaveApi: [] assinatura: [] requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Movimento' - type: object required: [amount, bet_id] properties: amount: allOf: - $ref: '#/components/schemas/Centavos' description: | O valor do movimento que se desfaz. Vai SEMPRE, e vale o mesmo em todas as tentativas do mesmo `transaction_id`: um estorno que mudasse de valor entre tentativas seria recusado como divergente na segunda. bet_id: type: string description: o movimento a desfazer reason: type: string description: diagnóstico; não influencia o processamento example: round_closed_after_debit responses: '200': description: 200 com status ok, ou 200 com status error — desfeito, ou nada a desfazer content: application/json: schema: { $ref: '#/components/schemas/Resposta' } '400': { $ref: '#/components/responses/Erro' } '401': { $ref: '#/components/responses/Erro' } # ------------------------------------------------------------------ o jogo --- # # A outra direcao. Servidor: https://jumbo.aircrash.net # # As duas exigem X-Signature quando o jogo tem segredo configurado, e recusam # com 401 INVALID_SIGNATURE tudo que nao casar -- assinatura ausente, assinatura # de outro segredo, corpo alterado, ou pedido fora da janela de 5 minutos. /v1/games: post: tags: [jogo] summary: O catalogo de jogos habilitados para o operador operationId: catalogo security: - assinatura: [] description: | Os tres limites -- `min_bet`, `max_bet`, `max_win` -- so aparecem quando o pedido traz `currency`, valem para AQUELA moeda, e sao centavos inteiros. **Um limite ausente quer dizer desconhecido, nunca que nao ha limite.** E por isso que ele e omitido em vez de sair zerado: um zero seria lido como aposta minima de zero, que e o que o servidor recusa aceitar. requestBody: required: true content: application/json: schema: type: object required: [operator, timestamp] properties: operator: { type: string } timestamp: type: integer format: int64 description: milissegundos desde a epoca; fora de 5 min e recusado currency: allOf: [{ $ref: '#/components/schemas/Moeda' }] description: sem ela, os tres limites nao saem responses: '200': description: o catalogo content: application/json: schema: type: object required: [status, currencies, games] properties: status: { type: string, enum: [ok] } operator: { type: string } currencies: type: array items: { $ref: '#/components/schemas/Moeda' } description: as moedas habilitadas para voce games: type: array items: type: object required: [game_id, name, studio, category] properties: game_id: { type: string } name: { type: string } studio: { type: string } category: { type: string } demo: { type: boolean } mobile: { type: boolean } desktop: { type: boolean } freebet: type: boolean description: aceita rodadas gratis concedidas pelo operador image_url: { type: string } currency: { $ref: '#/components/schemas/Moeda' } min_bet: allOf: [{ $ref: '#/components/schemas/Centavos' }] description: ausente = desconhecido, nunca "sem limite" max_bet: allOf: [{ $ref: '#/components/schemas/Centavos' }] description: ausente = desconhecido, nunca "sem limite" max_win: allOf: [{ $ref: '#/components/schemas/Centavos' }] description: teto de pagamento; premio maior e pago no teto '400': description: INTERNAL_ERROR (corpo ilegivel ou nao-JSON) ou MISSING_GAME_ID content: application/json: schema: { $ref: '#/components/schemas/ErroDoJogo' } '401': description: INVALID_SIGNATURE — ausente, de outro segredo, corpo alterado, ou fora da janela content: application/json: schema: { $ref: '#/components/schemas/ErroDoJogo' } '500': description: INTERNAL_ERROR content: application/json: schema: { $ref: '#/components/schemas/ErroDoJogo' } /v1/launch: post: tags: [jogo] summary: Abre uma partida para um jogador operationId: lancar security: - assinatura: [] description: | Devolve a URL para carregar em iframe ou redirecionar. **O token e o player_id nao vao na URL.** Eles viajam aqui, servidor a servidor, e a URL leva um ticket do jogo: aleatorio, de USO UNICO e valido por 15 minutos. Abrir a mesma URL duas vezes so abre uma sessao. O token tem de ser aleatorio: diferente do `player_id` e com ao menos 16 caracteres, sob pena de `INVALID_TOKEN`. Um token igual ao `player_id` nao e segredo nenhum -- e um numero que o operador imprime em toda tela. requestBody: required: true content: application/json: schema: type: object required: [operator, timestamp, game_id] properties: operator: { type: string } timestamp: { type: integer, format: int64 } game_id: type: string description: tem de ser o jogo desta instalacao token: type: string minLength: 16 description: obrigatorio, salvo em demo; vai em toda chamada de carteira player_id: type: string description: obrigatorio, salvo em demo currency: { $ref: '#/components/schemas/Moeda' } language: type: string enum: [pt, en, es] description: outro valor, ou ausente, cai no idioma padrao da instalacao player_name: type: string description: apelido ou nome; e ofuscado antes de aparecer na mesa theme: { type: string } demo: { type: boolean } mobile: { type: boolean } lobby_url: { type: string } deposit_url: { type: string } responses: '200': description: a partida content: application/json: schema: type: object required: [status, url] properties: status: { type: string, enum: [ok] } url: type: string format: uri description: abra em ate 15 minutos '400': description: > MISSING_GAME_ID, MISSING_TOKEN, MISSING_PLAYER_ID, INVALID_TOKEN, DEMO_NOT_AVAILABLE, ou INTERNAL_ERROR quando o corpo nao e JSON content: application/json: schema: { $ref: '#/components/schemas/ErroDoJogo' } '401': description: INVALID_SIGNATURE — ausente, de outro segredo, corpo alterado, ou fora da janela content: application/json: schema: { $ref: '#/components/schemas/ErroDoJogo' } '500': description: INTERNAL_ERROR content: application/json: schema: { $ref: '#/components/schemas/ErroDoJogo' } '404': description: GAME_NOT_FOUND content: application/json: schema: { $ref: '#/components/schemas/Erro' } components: securitySchemes: chaveApi: type: http scheme: bearer description: | Uma credencial, num sentido só: o operador a emite, e o jogo a apresenta ao chamar a carteira. Não existe chave no sentido contrário, porque não existe chamada no sentido contrário. No fio ela vai como `Authorization: Bearer ` — a palavra `Bearer` com B maiúsculo, um espaço simples, e a chave crua, sem base64 e sem aspas. Um parser que só aceite `bearer` minúsculo recusa todas as chamadas. O formato é escolha do operador; o jogo repassa a chave como texto opaco e não impõe nada. O usado nas chaves deste projeto é `avtr_wl_<8 de [a-z0-9]>_<43 de [A-Za-z0-9]>`: prefixo fixo que facilita varredura de segredo em log, identificador opaco que não muda na rotação, e 256 bits de entropia. Como nem o identificador nem o segredo contêm `_`, a leitura é uma divisão em quatro campos. A comparação deve ser feita em **tempo constante**: o tempo de resposta de uma comparação por igualdade revela o tamanho do prefixo correto, o que permite recuperar a chave por tentativas sucessivas. **Ao recusar, responda 401 com `{"status": "error", "error_code": "INVALID_SIGNATURE"}`.** O código nomeado é o que faz o jogo tratar a recusa como configuração dele quebrada, e não como sessão inválida do jogador — sem ele, uma chave errada vira "sua sessão expirou" na tela de todos os jogadores ao mesmo tempo. Um 4xx não abre o disjuntor do jogo, e a mensagem do operador nunca chega ao navegador: ela vai só para o log. Aceite **duas chaves válidas ao mesmo tempo** durante uma rotação. Do lado do jogo a chave é lida na subida, então aplicá-la é um reinício do processo, e sem a janela a troca é indisponibilidade. A chave não protege contra repetição de requisição, e permanece válida indefinidamente caso vaze de um log. HTTPS é obrigatório — o jogo recusa subir com uma carteira em `http://` fora de loopback — e restringir a origem, do lado do operador, é a camada que continua servindo depois de um vazamento. assinatura: type: apiKey in: header name: X-Signature description: | `hex(HMAC-SHA256(corpo, segredo))`, em minusculas, sobre **os bytes exatos do corpo** da requisicao. Assine o que vai no fio e confira sobre esses mesmos bytes. Decodificar o JSON e serializa-lo de novo antes do HMAC muda o espacamento e a ordem das chaves, e a assinatura deixa de bater com um conteudo identico. E o erro mais comum, nos dois lados, e ele nao aparece em teste com corpo pequeno: aparece no primeiro corpo cujo re-encode reordene uma chave. O `timestamp` que fecha a janela de 5 minutos vai **dentro do corpo assinado**, em milissegundos, e nao num cabecalho proprio. Poe-lo num cabecalho o deixaria de fora do HMAC, e um intermediario poderia reescreve-lo sem quebrar a assinatura. Vale nas duas direcoes. Nas rotas do JOGO (`/v1/games`, `/v1/launch`) ela e a UNICA autenticacao: nao ha chave nossa a apresentar, e por isso o jogo recusa subir em modo operador sem segredo configurado. Recuse com `401` e `INVALID_SIGNATURE` tudo que nao casar: assinatura ausente, assinatura de outro segredo, corpo alterado, ou pedido fora da janela. schemas: Moeda: type: string description: ISO 4217 example: BRL Centavos: type: integer format: int64 minimum: 0 description: | Inteiro, em centavos. `R$ 3,56` é `356`. Campo monetário não admite ponto flutuante. example: 356 Envelope: type: object required: [operator, timestamp, request_id] description: | Vai em **toda** chamada que o jogo faz a carteira, inclusive no `/balance`. Os quatro campos sao preenchidos a cada TENTATIVA, e nao a cada movimento. properties: operator: type: string description: quem esta chamando; a mesma carteira pode atender varias marcas example: casa-do-norte timestamp: type: integer format: int64 description: | Milissegundos desde a epoca, **no corpo assinado**. Fora de uma janela de 5 minutos o pedido deve ser recusado com `INVALID_SIGNATURE`: e o que impede reenviar um pedido capturado. Ele muda a cada tentativa, de proposito. Uma retentativa pode sair horas depois de a primeira ter falhado, e um timestamp de enfileiramento chegaria velho justamente na hora em que ninguem esta olhando. example: 1755680000000 request_id: type: string description: | Nomeia a **tentativa**, nao o movimento. Duas tentativas do mesmo `transaction_id` levam `request_id` diferentes, e e assim que se acha UMA delas no log. Nao use como chave de idempotencia: a chave e o `transaction_id`. example: 9c3f21a0e5b74c18 game_id: type: string description: qual jogo; so temos um, mas o campo existe no contrato example: jumbo-aviator Base: allOf: - $ref: '#/components/schemas/Envelope' - type: object required: [player_id, token, currency] properties: player_id: { type: string, example: op-user-99321 } token: type: string description: o `sid` da sessão example: 0f0e0d0c-0b0a-4009-8008-070605040302 currency: { $ref: '#/components/schemas/Moeda' } Movimento: allOf: - $ref: '#/components/schemas/Base' - type: object required: [transaction_id, round_id] properties: transaction_id: type: string description: | Identifica o **movimento de valor** e não muda entre retentativas — é a chave de idempotência. Restrição única em `(player_id, transaction_id)`. example: d7e33f5e-c6e8-4df5-b190-3c032fba071a round_id: type: string description: agrupa os movimentos de uma rodada; não é único example: '1842' RespostaSaldo: type: object required: [status, balance, currency] properties: status: type: string enum: [ok] balance: allOf: [{ $ref: '#/components/schemas/Centavos' }] description: saldo **posterior** à operação currency: { $ref: '#/components/schemas/Moeda' } transaction_id: type: string description: > O identificador que o OPERADOR deu ao movimento — não o nosso. Existe para a conciliação: é o que se cita ao abrir um chamado sobre um movimento específico. Erro: type: object required: [status, error_code] description: > Recusa. Erro de NEGOCIO chega em HTTP 200 com `status: error`; erro de PROTOCOLO chega no status que lhe cabe (401 para assinatura, 400 para corpo malformado). A distincao existe porque 5xx e falha de TRANSPORTE e pode vir de um proxy que a carteira nunca viu: uma decisao mandada como 5xx e retentada para sempre. properties: status: type: string enum: [error] error_code: type: string description: | **A regra padrao e RETENTAR.** Um codigo fora desta lista, ou uma resposta sem codigo nenhum, e tratado como falha temporaria e volta ate 20 vezes. Sao FINAIS apenas estes sete: INSUFFICIENT_FUNDS TOKEN_EXPIRED TOKEN_INVALID PLAYER_NOT_FOUND BET_NOT_FOUND CURRENCY_MISMATCH PLAYER_BLOCKED `INTERNAL_ERROR` e `INVALID_SIGNATURE` estao no enum por serem parte do contrato, mas sao RETENTAVEIS: o primeiro por descrever falha passageira, o segundo porque a correcao e do nosso lado e a retentativa passa a valer assim que a configuracao for consertada. Nao invente codigo para recusar em definitivo. Uma recusa final com nome desconhecido vira 20 tentativas identicas contra a sua carteira. enum: - INSUFFICIENT_FUNDS - TOKEN_EXPIRED - TOKEN_INVALID - PLAYER_NOT_FOUND - BET_NOT_FOUND - CURRENCY_MISMATCH - PLAYER_BLOCKED - INTERNAL_ERROR - INVALID_SIGNATURE error_message: { type: string } balance: allOf: [{ $ref: '#/components/schemas/Centavos' }] description: opcional; aplicável em INSUFFICIENT_FUNDS Resposta: description: | O corpo de um `200` da carteira. Ele e uma das duas coisas, e o que distingue e o campo `status`: status: "ok" -> deu certo, e `balance` e o saldo posterior status: "error" -> foi RECUSADO, e `error_code` diz por que Um cliente que so olhe o status HTTP le uma recusa como sucesso. Ramifique em `status`, sempre, antes de olhar `balance`. oneOf: - $ref: '#/components/schemas/RespostaSaldo' - $ref: '#/components/schemas/Erro' discriminator: propertyName: status mapping: ok: '#/components/schemas/RespostaSaldo' error: '#/components/schemas/Erro' ErroDoJogo: type: object required: [status, error_code] description: | A recusa das rotas do JOGO. Vem no status HTTP que lhe cabe, e nao em `200`: aqui nao ha decisao de negocio a comunicar, so pedido invalido. properties: status: type: string enum: [error] error_code: type: string enum: - MISSING_GAME_ID # 400 - MISSING_TOKEN # 400 - MISSING_PLAYER_ID # 400 - INVALID_TOKEN # 400, token com menos de 16 chars ou igual ao player_id - DEMO_NOT_AVAILABLE # 400 - GAME_NOT_FOUND # 404 - INVALID_SIGNATURE # 401 - INTERNAL_ERROR # 400 (corpo ilegivel ou nao-JSON) e 500 description: | Disjunto do enum de `Erro`: nenhum destes aparece na direcao da carteira, e nenhum daquela aparece aqui. `INTERNAL_ERROR` com status `400` nao e contradicao: e o que sai quando o corpo nao pode nem ser lido como JSON, antes de haver campo que nomear. error_message: type: string description: diagnostico; nao e para exibir ao jogador responses: Erro: description: | Falha de PROTOCOLO, no status que lhe cabe. Erro de NEGOCIO nao vem por aqui: vem em `200`, com `status: error`. content: application/json: schema: { $ref: '#/components/schemas/Erro' }