openapi: 3.1.0

info:
  title: Sloteflix API
  version: "1.0.0"
  summary: API de integração do agregador de jogos Sloteflix.
  description: |
    A integração Sloteflix tem duas metades:

    1. **API Sloteflix** (documentada aqui em `paths`) — endpoints que **você chama**
       para listar o catálogo e abrir jogos.
    2. **Seamless Wallet** (documentada em `webhooks`) — o endpoint
       `POST /api/sloteflix_api` que **você implementa** na sua plataforma e que a
       Sloteflix chama para debitar apostas, creditar prêmios e estornar.

    ## Orçamento de latência do seamless

    O cliente HTTP que dispara os callbacks usa timeout de conexão de **100 ms**,
    timeout de resposta de **600 ms** e **3 tentativas** sem espera entre elas.
    Após 3 falhas, a Sloteflix sintetiza `{"status": 15}` (Timeout) e a rodada é
    rejeitada. Seu handler precisa responder em menos de 600 ms.

    ## Valores monetários

    Apostas e prêmios são **decimais na moeda do jogador**, com 2 casas — não centavos.
    Persistência interna: `DECIMAL(20,2)`.
  contact:
    name: Sloteflix — Integrações
  license:
    name: Proprietary

servers:
  - url: https://{host}/api/v1
    description: API Sloteflix
    variables:
      host:
        default: SLOTEFLIX_HOST
        description: Host entregue pelo time de integração no cadastro.

tags:
  - name: Autenticação
    description: Troca das credenciais de agente por um JWT.
  - name: Jogos
    description: Catálogo e abertura de jogos.
  - name: Provedores
    description: Provedores disponíveis.
  - name: Clone
    description: Trilha de distribuição alternativa.
  - name: Free spins
    description: Concessão e cancelamento de rodadas grátis.
  - name: Agents
    description: Provisionamento e gestão de agentes (administrativo).

security: []

paths:

  # ------------------------------------------------------------------
  /auth/authentication:
    post:
      tags: [Autenticação]
      summary: Obter token JWT
      operationId: authenticate
      description: |
        Envie `Authorization: Bearer base64("{agent_token}:{agent_secret_key}")`.
        Retorna um JWT para as rotas protegidas por `auth.jwt`.
      security:
        - agentBasic: []
      responses:
        "200":
          description: Token emitido.
          content:
            application/json:
              schema:
                type: object
                required: [access_token, token_type]
                properties:
                  access_token:
                    type: string
                    examples: ["eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."]
                  token_type:
                    type: string
                    examples: ["bearer"]
        "400":
          description: |
            `INVALID_TOKEN` — header ausente ou fora do formato `token:secret`.
            `INVALID_AGENT` — par não corresponde a nenhum agente.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CodedError" }

  # ------------------------------------------------------------------
  /games/game_launch:
    get:
      tags: [Jogos]
      summary: Abrir um jogo
      operationId: launchGame
      description: |
        Retorna uma URL pronta para iframe ou redirect. A Sloteflix resolve o
        distribuidor (ordem de prioridade, com fallback), cria a sessão e assina a URL.

        **Chame sempre do backend** — os parâmetros carregam o `agent_token`.
      parameters:
        - name: agent_code
          in: query
          required: true
          schema: { type: string }
          examples: { padrao: { value: "minhaoperacao" } }
        - name: agent_token
          in: query
          required: true
          schema: { type: string }
          description: Validado contra o `agent_code`.
        - name: game_id
          in: query
          required: true
          schema: { type: string }
          description: O `game_code` do catálogo — não o `id` numérico.
          examples: { padrao: { value: "vs20olympgate" } }
        - name: type
          in: query
          required: true
          schema: { type: string }
          description: Modo da sessão. Use `CHARGED` para dinheiro real.
          examples: { padrao: { value: "CHARGED" } }
        - name: currency
          in: query
          required: true
          schema: { type: string }
          description: ISO 4217. Precisa bater com a moeda do agente.
          examples: { padrao: { value: "BRL" } }
        - name: lang
          in: query
          required: true
          schema: { type: string }
          examples: { padrao: { value: "BR" } }
        - name: demo_agent
          in: query
          required: true
          schema: { type: integer, enum: [0, 1] }
          description: |
            `0` = dinheiro real, `1` = demo. **Obrigatório mesmo valendo `0`** —
            omitir gera erro de validação.
        - name: user_id
          in: query
          required: true
          schema: { type: string }
          description: Seu ID de jogador. Rejeitado se vazio ou `0`.
        - name: force_demo
          in: query
          required: false
          schema: { type: boolean }
          description: Força o caminho demo mesmo com `demo_agent=0`.
        - name: rtp
          in: query
          required: false
          schema: { type: integer }
          description: RTP alvo, quando o distribuidor suporta.
      responses:
        "200":
          description: URL de launch.
          content:
            application/json:
              schema:
                type: object
                required: [game_url]
                properties:
                  game_url:
                    type: string
                    format: uri
        "400":
          description: |
            Três formatos possíveis, todos com HTTP 400 — trate qualquer resposta
            sem `game_url` como falha.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CodedError"
                  - $ref: "#/components/schemas/MessageError"
                  - $ref: "#/components/schemas/ValidationError"

  # ------------------------------------------------------------------
  /games/all:
    get:
      tags: [Jogos]
      summary: Catálogo completo
      operationId: listGames
      description: |
        Todos os jogos ativos com metadados completos.

        **Sem paginação** — a resposta pode passar de vários MB. Rode em job de
        background, não no caminho de uma requisição de usuário.
      responses:
        "200":
          description: Catálogo.
          content:
            application/json:
              schema:
                type: object
                required: [status, games]
                properties:
                  status: { type: boolean, const: true }
                  games:
                    type: array
                    items: { $ref: "#/components/schemas/ProviderGame" }

  /games/games-status:
    get:
      tags: [Jogos]
      summary: Códigos dos jogos ativos
      operationId: listGameCodes
      description: Versão leve do catálogo — apenas os `game_code` ativos. Ideal para reconciliação frequente.
      responses:
        "200":
          description: Lista de códigos.
          content:
            application/json:
              schema:
                type: object
                required: [status, games]
                properties:
                  status: { type: boolean, const: true }
                  games:
                    type: array
                    items: { type: string }
                    examples:
                      - ["vs20olympgate", "vs20fruitsw", "spribe_aviator"]

  /games/to-add:
    post:
      tags: [Jogos]
      summary: Hidratar códigos específicos
      operationId: getGamesToAdd
      description: |
        Recebe uma lista de `game_code` e devolve os registros completos dos que
        estão ativos.

        O campo `games` é lido sem validação: corpo sem esse campo, ou com valor
        que não seja array, gera HTTP 500. Sempre envie um array.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [games]
              properties:
                games:
                  type: array
                  items: { type: string }
            examples:
              padrao:
                value: { games: ["vs20olympgate", "vs20fruitsw"] }
      responses:
        "200":
          description: Jogos encontrados.
          content:
            application/json:
              schema:
                type: object
                required: [status, games]
                properties:
                  status: { type: boolean, const: true }
                  games:
                    type: array
                    items: { $ref: "#/components/schemas/ProviderGame" }
        "500":
          description: Campo `games` ausente ou não-array.

  # ------------------------------------------------------------------
  /games/provider:
    get:
      tags: [Provedores]
      summary: Listar provedores
      operationId: listProviders
      description: Todos os provedores ativos. Requer o JWT de `/auth/authentication`.
      security:
        - bearerJWT: []
      responses:
        "200":
          description: Provedores.
          content:
            application/json:
              schema:
                type: object
                required: [status, providers]
                properties:
                  status: { type: boolean, const: true }
                  providers:
                    type: array
                    items: { $ref: "#/components/schemas/Provider" }
        "401":
          description: JWT ausente, inválido ou expirado.

  # ------------------------------------------------------------------
  /games/list-clone:
    get:
      tags: [Clone]
      summary: Listar jogos da trilha de clone
      operationId: listCloneGames
      description: O formato acompanha a resposta do distribuidor de clone.
      responses:
        "200":
          description: Jogos da trilha de clone.
          content:
            application/json:
              schema: { type: object, additionalProperties: true }

  /games/get-clone-status:
    post:
      tags: [Clone]
      summary: Consultar flag de clone
      operationId: getCloneStatus
      security:
        - agentBasic: []
      responses:
        "200":
          description: Valor atual.
          content:
            application/json:
              schema:
                type: object
                required: [status, is_cloned]
                properties:
                  status: { type: boolean, const: true }
                  is_cloned:
                    oneOf:
                      - { type: integer, enum: [0, 1] }
                      - { type: boolean }
        "400":
          description: "`INVALID_TOKEN`, `INVALID_TOKEN2` ou `INVALID_AGENT`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CodedError" }

  /games/change-clone-status:
    post:
      tags: [Clone]
      summary: Inverter flag de clone
      operationId: changeCloneStatus
      description: |
        **Toggle, não set.** Inverte o valor atual e retorna o novo. Um retry cego
        reverte a mudança — para definir um valor explícito use
        `POST /agents/toggle-status`, que aceita o campo `status`.
      security:
        - agentBasic: []
      responses:
        "200":
          description: Novo valor.
          content:
            application/json:
              schema:
                type: object
                required: [status, is_cloned]
                properties:
                  status: { type: boolean, const: true }
                  is_cloned: { type: boolean }
        "400":
          description: "`INVALID_TOKEN`, `INVALID_TOKEN2` ou `INVALID_AGENT`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CodedError" }

  # ------------------------------------------------------------------
  /games/freespin:
    post:
      tags: [Free spins]
      summary: Conceder free spins
      operationId: createFreeSpins
      description: |
        Disponível para jogos com `has_freespins = 1`.

        Os prêmios chegam como callback `transaction` com `type: "win"`, igual a um
        prêmio normal — não há callback específico de free spin.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_code, agent_token, user_id, game_id, betlevel, freespincount, expiretime, freespinID]
              properties:
                agent_code:    { type: string }
                agent_token:   { type: string }
                user_id:       { type: string }
                game_id:       { type: string, description: "O `game_code` do catálogo." }
                betlevel:      { type: integer, description: "Nível de aposta de cada rodada." }
                freespincount: { type: integer, description: "Quantidade de rodadas." }
                expiretime:    { type: string, description: "Momento de expiração." }
                freespinID:    { type: string, description: "Seu identificador. Guarde — é o que permite cancelar." }
                rtp:           { type: integer, description: "RTP alvo (opcional)." }
            examples:
              padrao:
                value:
                  agent_code: "minhaoperacao"
                  agent_token: "SEU_TOKEN"
                  user_id: "1042"
                  game_id: "vs20olympgate"
                  betlevel: 1
                  freespincount: 20
                  expiretime: "2026-08-30 23:59:59"
                  freespinID: "promo-2026-08-1042"
      responses:
        "200":
          description: Resultado do distribuidor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {}
        "400":
          description: Validação, agente/jogo inválido, ou sem distribuidor ativo.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/MessageError"
                  - $ref: "#/components/schemas/ValidationError"

  /games/cancelfreespin:
    post:
      tags: [Free spins]
      summary: Cancelar free spins
      operationId: cancelFreeSpins
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_code, agent_token, user_id, game_id, freespinID]
              properties:
                agent_code:  { type: string }
                agent_token: { type: string }
                user_id:     { type: string }
                game_id:     { type: string }
                freespinID:  { type: string, description: "O mesmo usado na criação." }
                rtp:         { type: integer }
      responses:
        "200":
          description: Resultado do distribuidor.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {}
        "400":
          description: Validação, agente/jogo inválido, ou sem distribuidor ativo.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/MessageError"
                  - $ref: "#/components/schemas/ValidationError"

  # ------------------------------------------------------------------
  /agents/create:
    post:
      tags: [Agents]
      summary: Criar agente
      operationId: createAgent
      description: |
        **Requer token administrativo mestre**, que vale para toda a plataforma.
        Operadores integrando a Sloteflix não recebem esse token.

        O `endpoint` gerado é um **placeholder** derivado do `subdomain`
        (`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.
      security:
        - masterToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, email, password, subdomain]
              properties:
                token:     { type: string, description: "SLOTEFLIX_MASTER_TOKEN" }
                email:     { type: string, format: email }
                password:  { type: string, minLength: 6 }
                subdomain: { type: string, description: "Vira o `agent_code` e a base do `endpoint`." }
      responses:
        "200":
          description: Criado, ou já existente (mensagem distingue os casos).
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:      { type: integer }
                  message: { type: string }
                  data:    { $ref: "#/components/schemas/Agent" }
        "400":
          description: Validação, ou usuário existente sem agente vinculado.
        "403":
          description: Token mestre inválido.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminError" }

  /agents/consult:
    post:
      tags: [Agents]
      summary: Relatório de GGR por provedor
      operationId: consultAgentStats
      description: |
        Totais de aposta e prêmio por provedor no intervalo. Exclui transações de
        afiliado. Base da reconciliação diária.

        **Período sem movimento devolve HTTP 404**, não `200` com array vazio.
      security:
        - masterToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, agent_code, start_date, end_date]
              properties:
                token:      { type: string }
                agent_code: { type: string }
                start_date: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' }
                end_date:   { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' }
      responses:
        "200":
          description: Totais por provedor.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        provider:  { type: string }
                        total_bet: { type: string, description: "Decimal serializado como string." }
                        total_win: { type: string }
        "400":
          description: Validação (formato de data).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ValidationError" }
        "403":
          description: Token mestre inválido.
        "404":
          description: Agente não encontrado, **ou** nenhum dado no período.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminError" }

  /agents/get-agent-data:
    post:
      tags: [Agents]
      summary: Dados do próprio agente
      operationId: getAgentData
      description: |
        O único endpoint de agentes que você chama com as **suas** credenciais.
        Serve como health check das credenciais.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_code, agent_token]
              properties:
                agent_code:  { type: string }
                agent_token: { type: string }
      responses:
        "200":
          description: Identificação do agente.
          content:
            application/json:
              schema:
                type: object
                required: [id, agent_code]
                properties:
                  id:         { type: integer }
                  agent_code: { type: string }
        "400":
          description: "`Agent not found` ou `Invalid agent token`."
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessageError" }

  /agents/get-status:
    post:
      tags: [Agents]
      summary: Consultar flag de clone do agente
      operationId: getAgentStatus
      description: |
        O campo `status` da resposta reflete `is_cloned` — **não** o `agent_status`
        (ativo/inativo).
      security:
        - masterToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, agent_code]
              properties:
                token:      { type: string }
                agent_code: { type: string }
      responses:
        "200":
          description: Status atual.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentStatusResponse" }
        "400":
          description: Validação.
        "403":
          description: Token mestre inválido.
        "404":
          description: Agente não encontrado.

  /agents/toggle-status:
    post:
      tags: [Agents]
      summary: Definir ou inverter a flag de clone
      operationId: toggleAgentStatus
      description: |
        Com `status` presente, define o valor. **Sem `status`, inverte o atual** —
        em automação sempre envie explicitamente, senão um retry desfaz a mudança.
      security:
        - masterToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, agent_code]
              properties:
                token:      { type: string }
                agent_code: { type: string }
                status:     { type: boolean, description: "Se omitido, inverte o valor atual." }
      responses:
        "200":
          description: Novo status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentStatusResponse" }
        "400":
          description: Validação.
        "403":
          description: Token mestre inválido.
        "404":
          description: Agente não encontrado.

# ====================================================================
# SEAMLESS WALLET — você implementa
# ====================================================================
webhooks:

  sloteflix_api:
    post:
      tags: [Seamless]
      summary: "POST {seu_endpoint}/api/sloteflix_api"
      operationId: seamlessWallet
      description: |
        **Você implementa este endpoint.** Um único caminho recebe todos os eventos
        financeiros, roteados pelo campo `method` (e por `type`, quando
        `method = transaction`).

        ## URL de destino

        A Sloteflix monta o destino a partir do campo `endpoint` do seu agente:

        1. substitui o domínio `lumosgaming.com` por `connectionslm.com`;
        2. remove a barra final;
        3. concatena `/api/sloteflix_api` (caminho **fixo**, não configurável).

        ## Autenticação

        Não há HMAC nem header de autenticação. A credencial é o trio
        `agent_code` + `agent_token` + `agent_secret_key` **no corpo**. Valide os
        três com comparação em tempo constante e rejeite se qualquer um divergir.

        ## Resposta

        Sempre `HTTP 200` com JSON contendo `status` numérico e `balance`.
        Nunca HTML, nunca corpo vazio, nunca outro status HTTP — inclusive no
        caminho de exceção.

        ## Idempotência

        A Sloteflix reenvia o **mesmo** `transaction_id` em até 3 tentativas.
        Repetição deve devolver `status: 1` com o saldo atual — **não** `status: 13`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/SeamlessAccountDetails"
                - $ref: "#/components/schemas/SeamlessUserBalance"
                - $ref: "#/components/schemas/SeamlessBet"
                - $ref: "#/components/schemas/SeamlessWin"
                - $ref: "#/components/schemas/SeamlessRefund"
                - $ref: "#/components/schemas/SeamlessRollback"
              discriminator:
                propertyName: method
            examples:
              account_details:
                summary: Abertura da sessão
                value:
                  method: account_details
                  game: vs20olympgate
                  agent_code: minhaoperacao
                  agent_token: SEU_TOKEN
                  agent_secret_key: SUA_SECRET
                  user_id: "1042"
              user_balance:
                summary: Consulta de saldo
                value:
                  method: user_balance
                  game: vs20olympgate
                  agent_code: minhaoperacao
                  agent_token: SEU_TOKEN
                  agent_secret_key: SUA_SECRET
                  user_id: "1042"
              bet:
                summary: Aposta (débito)
                value:
                  method: transaction
                  type: bet
                  session_id: eyJpdiI6IkQr
                  game: vs20olympgate
                  agent_code: minhaoperacao
                  agent_token: SEU_TOKEN
                  agent_secret_key: SUA_SECRET
                  round_id: timeless-88213004
                  transaction_id: timeless-99551277
                  bet: 1.50
                  win: 0
                  user_id: "1042"
                  distributor: timeless
              win:
                summary: Prêmio (crédito)
                value:
                  method: transaction
                  type: win
                  session_id: eyJpdiI6IkQr
                  game: vs20olympgate
                  agent_code: minhaoperacao
                  agent_token: SEU_TOKEN
                  agent_secret_key: SUA_SECRET
                  transaction_id: timeless-99551301
                  transaction_id_old: timeless-88213004
                  round_id: timeless-88213004
                  bet: 0
                  win: 45.00
                  user_id: "1042"
                  distributor: timeless
              refund:
                summary: Estorno de aposta
                value:
                  method: refund
                  agent_code: minhaoperacao
                  agent_token: SEU_TOKEN
                  agent_secret_key: SUA_SECRET
                  transaction_id: timeless-99551277
                  user_id: "1042"
                  distributor: timeless
                  balance: true
              rollback:
                summary: Reversão de crédito
                value:
                  method: rollback
                  agent_code: minhaoperacao
                  agent_token: SEU_TOKEN
                  agent_secret_key: SUA_SECRET
                  user_id: "1042"
                  transaction_id: betby-77120043
                  amount: 45.00
                  distributor: betby
      responses:
        "200":
          description: |
            **Sempre 200.** O resultado vai no campo `status` do corpo.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SeamlessResponse" }
              examples:
                sucesso:
                  summary: Operação concluída
                  value: { status: 1, balance: 1497.00 }
                saldo_insuficiente:
                  summary: Saldo insuficiente (só em aposta)
                  value: { status: 0, balance: 0.80, error: "Insufficient funds" }
                repetida:
                  summary: transaction_id já processado — devolva sucesso
                  value: { status: 1, balance: 1497.00 }
                jogador_bloqueado:
                  summary: Autoexcluído ou suspenso
                  value: { status: 11, balance: 1500.00, error: "Player is blocked" }
                erro_interno:
                  summary: Falha sua — prefira isto a JSON inválido
                  value: { status: 5, balance: 0, error: "Internal error" }

components:

  securitySchemes:
    agentBasic:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer base64("{agent_token}:{agent_secret_key}")`.
        Note que é Base64 simples, não Basic Auth com usuário/senha.
    bearerJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT obtido em `POST /auth/authentication`.
    masterToken:
      type: apiKey
      in: header
      name: X-Master-Token
      description: |
        **Atenção:** na implementação atual o token mestre vai no campo `token`
        do **corpo** da requisição, não em header. Este esquema existe só para
        marcar quais operações são administrativas — envie o valor em
        `body.token`.

  schemas:

    # ---------------- Erros da API Sloteflix ----------------
    CodedError:
      type: object
      description: Erro com código fixo. HTTP 400.
      required: [status, error]
      properties:
        status: { type: boolean, const: false }
        error:
          type: string
          enum: [INVALID_TOKEN, INVALID_TOKEN2, INVALID_AGENT, INVALID_GAME]

    MessageError:
      type: object
      description: Erro com mensagem livre. HTTP 400.
      required: [error]
      properties:
        error:
          type: string
          examples:
            - Agent not found
            - Invalid agent token
            - Invalid user ID
            - Game not found or inactive
            - No active distributor found for this game

    ValidationError:
      type: object
      description: Formato padrão do validador Laravel. HTTP 400.
      additionalProperties:
        type: array
        items: { type: string }
      examples:
        - { game_id: ["The game id field is required."] }

    AdminError:
      type: object
      description: Erro dos endpoints administrativos.
      required: [message]
      properties:
        message:
          type: string
          examples:
            - "Acesso não autorizado. Token inválido."
            - "Agente não encontrado."
            - "Nenhum dado encontrado para o período especificado."

    # ---------------- Catálogo ----------------
    ProviderGame:
      type: object
      properties:
        id:              { type: integer, description: "ID interno. Não use no launch." }
        provider_id:     { type: integer }
        game_server_url: { type: [string, "null"] }
        game_id:         { type: string, description: "ID no provedor original." }
        game_name:       { type: string }
        game_code:       { type: string, description: "Único. É o que você passa como `game_id` no launch." }
        game_type:       { type: [string, "null"], examples: ["slot"] }
        description:     { type: [string, "null"] }
        cover:           { type: string, format: uri }
        status:          { type: string, description: "`\"1\"` = ativo." }
        technology:      { type: [string, "null"], examples: ["html5"] }
        has_lobby:       { type: integer, enum: [0, 1] }
        is_mobile:       { type: integer, enum: [0, 1] }
        has_freespins:   { type: integer, enum: [0, 1] }
        has_tables:      { type: integer, enum: [0, 1] }
        only_demo:       { type: integer, enum: [0, 1] }
        distribution:    { type: string }
        created_at:      { type: string, format: date-time }
        updated_at:      { type: string, format: date-time }

    Provider:
      type: object
      properties:
        id:     { type: integer }
        name:   { type: string, examples: ["Pragmatic Play"] }
        code:   { type: string, examples: ["pragmatic-play"] }
        status: { type: integer, enum: [0, 1] }

    # ---------------- Agente ----------------
    Agent:
      type: object
      properties:
        id:               { type: integer }
        agent_sponsor_id: { type: integer }
        agent_id:         { type: integer, description: "FK para o usuário." }
        agent_code:       { type: string }
        agent_token:      { type: string, description: "32 caracteres." }
        agent_secret_key: { type: string, description: "32 caracteres." }
        agent_type:       { type: string, examples: ["default", "reseller"] }
        agent_status:     { type: integer, enum: [0, 1] }
        api_type:         { type: string, const: seamless }
        balance:          { type: number, description: "Crédito do agente junto ao agregador — não saldo de jogador." }
        currency:         { type: string, default: BRL }
        endpoint:         { type: string, format: uri, description: "Base URL do seu seamless. Sem barra final, sem path." }
        ggr:              { type: number }
        ggr_cloned:       { type: number }
        is_cloned:        { type: integer, enum: [0, 1] }

    AgentStatusResponse:
      type: object
      required: [message, data]
      properties:
        message: { type: string }
        data:
          type: object
          properties:
            id:         { type: integer }
            agent_code: { type: string }
            status:     { type: boolean, description: "Reflete `is_cloned`." }

    # ---------------- Seamless: base ----------------
    SeamlessCredentials:
      type: object
      description: Trio de credenciais presente em todo callback.
      required: [method, agent_code, agent_token, agent_secret_key]
      properties:
        method:           { type: string }
        agent_code:       { type: string }
        agent_token:      { type: string }
        agent_secret_key: { type: string }

    SeamlessAccountDetails:
      allOf:
        - $ref: "#/components/schemas/SeamlessCredentials"
        - type: object
          description: Abertura da sessão. Se falhar, o jogo não abre.
          required: [user_id]
          properties:
            method:  { const: account_details }
            game:    { type: string }
            user_id: { type: string }

    SeamlessUserBalance:
      allOf:
        - $ref: "#/components/schemas/SeamlessCredentials"
        - type: object
          description: |
            Consulta de saldo — o callback mais frequente. Otimize este caminho.
            Resposta sem `balance` é convertida em `OP_31 Insufficient funds`.
          required: [user_id]
          properties:
            method:  { type: string, enum: [user_balance, balance] }
            game:    { type: string }
            user_id: { type: string }

    SeamlessBet:
      allOf:
        - $ref: "#/components/schemas/SeamlessCredentials"
        - type: object
          description: Débito de aposta. Deve ser atômico e idempotente.
          required: [type, transaction_id, bet, user_id]
          properties:
            method:         { const: transaction }
            type:           { const: bet }
            session_id:     { type: string }
            game:           { type: string }
            round_id:       { type: string, description: "Agrupa aposta e prêmio da rodada." }
            transaction_id: { type: string, description: "Chave de idempotência. Único globalmente, prefixado pelo distribuidor." }
            bet:            { type: number, description: "Valor a debitar. Decimal, 2 casas, moeda do jogador." }
            win:            { type: number, const: 0 }
            user_id:        { type: string }
            distributor:    { type: string, examples: ["timeless", "betby", "clone"] }

    SeamlessWin:
      allOf:
        - $ref: "#/components/schemas/SeamlessCredentials"
        - type: object
          description: |
            Crédito de prêmio. **Nunca rejeite por saldo insuficiente** — crédito
            só aumenta saldo. Rodadas sem prêmio não geram este callback.
          required: [type, transaction_id, win, user_id]
          properties:
            method:             { const: transaction }
            type:               { const: win }
            session_id:         { type: string }
            game:               { type: string }
            transaction_id:     { type: string, description: "Chave de idempotência do crédito." }
            transaction_id_old: { type: string, description: "Aponta para a rodada de origem." }
            round_id:           { type: string }
            bet:                { type: number, const: 0 }
            win:                { type: number, description: "Valor a creditar." }
            user_id:            { type: string }
            distributor:        { type: string }

    SeamlessRefund:
      allOf:
        - $ref: "#/components/schemas/SeamlessCredentials"
        - type: object
          description: |
            Estorno de aposta. **O payload varia por distribuidor** — `user_id` e
            `balance` podem não vir. Resolva o jogador pelo `transaction_id`.
          required: [transaction_id]
          properties:
            method:         { const: refund }
            transaction_id: { type: string, description: "Aponta para a transação ORIGINAL a estornar." }
            user_id:        { type: string, description: "Pode estar ausente." }
            balance:        { type: boolean, description: "Quando `true`, o agregador lê `balance` da resposta. Pode estar ausente." }
            distributor:    { type: string }

    SeamlessRollback:
      allOf:
        - $ref: "#/components/schemas/SeamlessCredentials"
        - type: object
          description: |
            Reversão de crédito já pago — **retira** valor do jogador. Exige
            estritamente `status: 1` na resposta.
          required: [transaction_id, amount]
          properties:
            method:         { const: rollback }
            transaction_id: { type: string }
            amount:         { type: number, description: "Valor a retirar, positivo. Note: `amount`, não `bet`/`win`." }
            user_id:        { type: string }
            distributor:    { type: string }

    SeamlessResponse:
      type: object
      description: |
        Resposta que você devolve. `balance` deve estar presente **inclusive em
        erro** — vários caminhos do agregador testam esse campo antes do `status`.
      required: [status, balance]
      properties:
        status:
          type: integer
          description: |
            `1` = sucesso. Outros valores mapeiam para os códigos `OP_xx` que o
            provedor mostra ao jogador:

            | status | código   | significado                |
            |--------|----------|----------------------------|
            | 1      | —        | sucesso                    |
            | 0      | `OP_31`  | Insufficient funds         |
            | 2      | `OP_20`  | Invalid hash               |
            | 4      | `OP_34`  | Player not found           |
            | 5      | `OP_50`  | Internal error             |
            | 6      | `OP_50`  | Unknown error              |
            | 7      | `OP_21`  | Invalid token              |
            | 8      | `OP_22`  | Authorization failed       |
            | 9      | `OP_30`  | Invalid currency           |
            | 10     | `OP_32`  | Limits reached             |
            | 11     | `OP_33`  | Player is blocked          |
            | 12     | `OP_35`  | Game is disabled           |
            | 13     | `OP_40`  | Duplicated transaction ID  |
            | 14     | `OP_41`  | Transaction not found      |
            | 15     | `OP_47`  | Timeout (reservado)        |
            | 16     | `OP_49`  | Operation failed           |
            | 17     | `OP_50`  | Internal error             |

            Valores fora da tabela caem em `OP_50`.

            **Não use `13` para transação repetida** — devolva `1` com o saldo atual.
          enum: [0, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
        balance:
          type: number
          description: Saldo do jogador DEPOIS da operação. Numérico, 2 casas — não string.
        error:
          type: string
          description: Mensagem legível. Ajuda no diagnóstico, não altera comportamento.
        message:
          type: string
          description: Opcional, aceito em `account_details`.
        user_id:
          type: string
          description: Recomendado ecoar em `account_details`.
        email:
          type: string
          description: Opcional em `account_details`.
        date:
          type: string
          description: Opcional em `account_details`.
