openapi: 3.0.3
info:
  title: API de Integração de Entregas — Vainabag
  version: "1.0.0"
  description: |
    API pública pra apps de terceiro (ex: sistemas de pedido/cardápio digital externos)
    criarem e gerenciarem entregas na plataforma Vainabag, sem precisar acessar o painel.

    ## Acesso de testes (desenvolvedores)
    Ainda não tem uma loja na Vainabag? Peça acesso em
    [vainabag.com.br/desenvolvedores](https://vainabag.com.br/desenvolvedores). Depois
    da aprovação, você recebe por e-mail um `client_id`/`client_secret` de uma loja de
    testes só sua. Com essa credencial **toda entrega é de teste** (mesmo sem mandar
    `"test": true`): nunca chama entregador real nem movimenta dinheiro, e avança pelas
    rotas `/orders/{id}/simulate/*`. Cadastre sua URL de webhook com `POST /webhooks`.

    ## Parceiros (plataformas com várias lojas)
    Se o seu sistema atende várias lojas (ex: plataforma de pedidos), peça acesso como
    **parceiro** em [vainabag.com.br/desenvolvedores](https://vainabag.com.br/desenvolvedores).
    Aprovado, você recebe **uma** credencial de produção (`epk_...`/`eps_...`) que vale
    pra todas as lojas conectadas a você:
    1. O lojista conecta o seu sistema no painel Vainabag dele (botão **Integrações** →
       aba **Parceiros**) e recebe um **código da loja** (`vb_...`), que ele informa a você.
       Passo a passo com imagens na seção **Parceiros** abaixo.
    2. A conexão já vale na hora (o administrador da cidade pode pausá-la depois, se precisar).
    3. Gere o token com a sua credencial em `POST /auth/token` e mande o código da loja no
       header `X-Vainabag-Store` em todas as chamadas de entregas/carteira/taxas. Sem o
       header, a resposta é `400 STORE_REQUIRED`; loja não conectada ou ainda não aprovada
       responde `404 STORE_NOT_CONNECTED` ou `401`.
    4. `GET /partner/stores` lista as lojas conectadas e o status de cada uma.
    5. Cadastre **uma** URL em `/partner/webhooks` (mesmos campos de `/webhooks`): ela recebe
       os eventos de todas as lojas, com `storeCode` no corpo.

    ## Autenticação
    OAuth2 `client_credentials`. Lojistas geram a credencial (`client_id` + `client_secret`)
    no painel, em **Configurações de Entregas → Integrações → API de Entregas** (só
    disponível se o Owner do App tiver liberado o recurso). Troque a credencial por
    um token em `POST /auth/token` e mande esse token em todas as outras chamadas via
    `Authorization: Bearer <token>`. O token expira em 1h — gere outro quando expirar
    (não existe refresh token, é só repetir o client_credentials).

    **Toda credencial nova nasce pendente de aprovação** — o Owner precisa aprová-la
    (aba Integrações → API de Entregas do painel dele) antes de `POST /auth/token`
    funcionar. Enquanto pendente (ou se o Owner pausar depois), a troca de token
    devolve `403 client_not_approved`.

    ## Idempotência
    `POST /orders` exige o header `Idempotency-Key`. Repetir a mesma chave pra mesma
    credencial devolve a MESMA entrega já criada (200), em vez de criar uma nova —
    seguro pra retry de rede.

    ## Sandbox
    Toda entrega criada com `"test": true` nasce isolada — nunca gera oferta/notificação
    pra um entregador real, nunca é cobrada de verdade. Ela só progride pelas rotas
    `/orders/{id}/simulate/*`, que simulam cada etapa (pronto, aceite, coleta, entrega,
    saldo insuficiente, cancelamento) sem envolver ninguém de verdade — inclusive
    disparando o MESMO webhook de saída que uma corrida real dispararia, pra testar a
    integração de ponta a ponta.

    ## Webhooks de saída
    Cadastre uma URL pela própria API (`POST /webhooks`) ou no painel do lojista, em
    Configurações de Entregas → Integrações → API de Entregas → Webhooks de saída. Cada evento dispara um `POST` assinado
    (`X-Vainabag-Signature: sha256=<hmac>`, chave é o secret mostrado na hora do
    cadastro) com `X-Vainabag-Event` indicando o tipo:
    - `order.status_changed` — a entrega mudou de status (PENDENTE/EM_PREPARO/PRONTO/
      EM_ROTA/ENTREGUE/CANCELADO), não importa quem mudou: você (`PATCH .../status`),
      o entregador no app dele aceitando/coletando/finalizando, ou o lojista mudando
      manualmente no painel. Cobre real e sandbox (`/simulate/*`).
    - `order.driver_assigned` — um entregador foi atribuído à entrega (aceite pelo app do
      entregador, atribuição manual pelo lojista/operação, encaminhamento ou, no sandbox,
      `/simulate/driver-accept`). O corpo traz `driverName`.
    - `wallet.deposit_confirmed` — uma recarga criada via `POST /wallet/deposit`
      confirmou o pagamento Pix (nunca dispara pra depósito feito pelo lojista direto
      no painel dele).
servers:
  - url: https://api-pro.vainabag.com.br/integrations/v1
security:
  - bearerAuth: []
tags:
  - name: Autenticação
  - name: Entregas
  - name: Sandbox
  - name: Taxas
  - name: Carteira
  - name: Webhooks
  - name: Parceiros
    description: |
      Rotas exclusivas da credencial de **parceiro** (`epk_...`). Antes de enviar pedidos de uma
      loja, ela precisa se conectar ao seu sistema e te passar o **código da loja**. Mande este
      passo a passo ao lojista:

      ### Como o lojista conecta o seu sistema

      **1. Abrir as integrações.** No painel Vainabag, na tela de entregas, o lojista clica no botão
      **Integrações** (ícone de globo, ao lado da engrenagem).

      ![Botão Integrações no painel do lojista](https://vainabag.com.br/img/tutorial/conectar-parceiro-1.png)

      **2. Conectar o parceiro.** Na aba **Parceiros**, ele encontra o seu sistema na lista (com o
      nome e a logo que você enviou no cadastro) e clica em **Conectar**. A conexão vale na hora,
      sem aprovação.

      ![Aba Parceiros com o botão Conectar](https://vainabag.com.br/img/tutorial/conectar-parceiro-2.png)

      **3. Copiar o código da loja.** O sistema aparece como **Conectado**, com o **código da loja**
      (`vb_...`). O lojista clica em **Copiar** e envia o código a você.

      ![Parceiro conectado com o código da loja](https://vainabag.com.br/img/tutorial/conectar-parceiro-3.png)

      ### O que você faz com o código

      Guarde o código junto do cadastro da loja no seu sistema e mande-o no header
      `X-Vainabag-Store` em todas as chamadas de entregas, carteira e taxas daquela loja. Use
      `GET /partner/stores` para conferir as lojas conectadas e o status de cada uma.

      Se o lojista clicar em **Desconectar**, ou se o administrador da cidade pausar a conexão, as
      chamadas com esse código passam a responder `404 STORE_NOT_CONNECTED` ou `401`.

paths:
  /auth/token:
    post:
      tags: [Autenticação]
      summary: Troca client_id/client_secret por um access token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_id, client_secret, grant_type]
              properties:
                client_id:
                  type: string
                  example: eak_69a32f4b58c5ff3c56c6e2b3c1be9505
                client_secret:
                  type: string
                  example: eas_608c3fb25f4c3aa00e65e064f4a78665cd3363114987e2bd
                grant_type:
                  type: string
                  enum: [client_credentials]
      responses:
        "200":
          description: Token emitido
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token: { type: string }
                  token_type: { type: string, example: bearer }
                  expires_in: { type: integer, example: 3600 }
        "400":
          description: grant_type não suportado
        "401":
          description: client_id/client_secret inválidos, revogados, ou módulo desligado
        "403":
          description: |
            Credencial válida mas ainda não aprovada pelo Owner (ou pausada por ele) — toda
            credencial nova nasce pendente de aprovação. Peça pro lojista checar o status na
            aba Integrações → API de Entregas dele, ou falar com o suporte.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, example: client_not_approved }
                  message: { type: string }

  /orders:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Entregas]
      summary: Cria uma entrega (por padrão nasce em PENDENTE)
      description: |
        Por padrão a entrega nasce em `PENDENTE`, igual sempre foi. Mas se o seu sistema
        só manda o pedido pra cá depois que ele já foi preparado/confirmado do seu lado,
        dá pra pular direto pra `EM_PREPARO` ou `PRONTO` mandando `status` no corpo — evita
        ter que chamar `POST /orders` e depois `PATCH /orders/{id}/status` em sequência.

        - `status: "EM_PREPARO"` — não cobra nada, só avisa que já começou o preparo.
        - `status: "PRONTO"` — **cobra a taxa de entrega da Carteira de Entregas do
          lojista na hora**, mesma regra de sempre (ver `PATCH /orders/{id}/status`). Se o
          saldo do lojista NÃO cobrir, a criação **não é recusada** — a entrega é criada
          normalmente, só que fica em `PENDENTE` (não em `PRONTO`), e a resposta (`201`)
          vem com um campo extra `warning` avisando disso. Repita depois com `PATCH
          .../status {"status":"PRONTO"}` quando o lojista tiver depositado.
        - Omitir `status` (ou mandar `"PENDENTE"`) é o comportamento de sempre.
        - Em entrega sandbox (`test: true`) o campo `status` é **ignorado** — sandbox
          nasce sempre em `PENDENTE` e só progride pelas rotas `/simulate/*`, nunca por
          essa cobrança automática (garante que nenhum entregador real é despachado por
          causa de um teste).

        `bloqueioEnabled`/`refrigeranteEnabled` só têm efeito se o **lojista** tiver o
        modo correspondente ligado nas Configurações de Entregas dele (Modo Bloqueio /
        Modo Refrigerante). Se o lojista não ativou o modo, o campo é simplesmente
        ignorado (a entrega nasce sem esse override), mesmo que você mande `true` — não
        dá erro, só não tem efeito.
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string }
          description: Repetir a mesma chave devolve a mesma entrega já criada (200), não cria outra.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderCreate"
            examples:
              pendente:
                summary: Padrão — nasce em PENDENTE
                value: { clientName: "Maria", clientPhone: "11999999999", address: { neighborhood: "Centro" }, paymentMethod: "PIX" }
              jaPronta:
                summary: Já nasce PRONTA (cobra a taxa na hora)
                value: { clientName: "Maria", clientPhone: "11999999999", address: { neighborhood: "Centro" }, paymentMethod: "PIX", status: "PRONTO" }
      responses:
        "201":
          description: Entrega criada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
              examples:
                pronta:
                  summary: Criada direto em PRONTO — saldo cobriu a taxa
                  value: { id: "cmt9k2p3f0001abcd1234efgh", status: "PRONTO" }
                saldoInsuficiente:
                  summary: "Pediu PRONTO mas o saldo não cobriu — criada em PENDENTE mesmo assim, com aviso"
                  value:
                    id: "cmt9k2p3f0001abcd1234efgh"
                    status: "PENDENTE"
                    warning: { code: "INSUFFICIENT_WALLET_BALANCE", message: "Entrega criada em PENDENTE — saldo insuficiente na Carteira de Entregas" }
        "200":
          description: Já existia uma entrega com essa Idempotency-Key — devolvida sem criar outra
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "400":
          description: Erro de validação (campo faltando, bairro não encontrado, forma de pagamento inválida, status de criação inválido etc.)
    get:
      tags: [Entregas]
      summary: Lista as entregas criadas por esta credencial
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [PENDENTE, EM_PREPARO, PRONTO, EM_ROTA, ENTREGUE, CANCELADO] }
        - in: query
          name: page
          schema: { type: integer, default: 1 }
        - in: query
          name: pageSize
          schema: { type: integer, default: 20, maximum: 100 }
      responses:
        "200":
          description: Lista paginada
          content:
            application/json:
              schema:
                type: object
                properties:
                  orders:
                    type: array
                    items: { $ref: "#/components/schemas/Order" }
                  total: { type: integer }
                  page: { type: integer }
                  pageSize: { type: integer }
                  totalPages: { type: integer }

  /orders/{id}:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    get:
      tags: [Entregas]
      summary: Consulta uma entrega
      parameters:
        - $ref: "#/components/parameters/OrderId"
      responses:
        "200":
          description: Entrega encontrada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
        "404":
          description: Não encontrada (ou não pertence a esta credencial)

  /orders/{id}/status:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    patch:
      tags: [Entregas]
      summary: Muda o status de uma entrega (não funciona em entrega sandbox — use /simulate)
      description: |
        `POST /orders` sempre cria a entrega em `PENDENTE` — nada acontece do nosso lado
        até você chamar essa rota. É ela quem move a entrega adiante, e o lojista **não
        precisa entrar no painel** pra nenhuma dessas transições: o ciclo de vida normal
        é inteiramente controlado pelo seu sistema, chamando essa rota em sequência:

        1. `{"status": "EM_PREPARO"}` — avisa que começou o preparo. Não cobra nada.
        2. `{"status": "PRONTO"}` — avisa que está pronta pra coleta. **É aqui que a taxa
           de entrega é cobrada** da Carteira de Entregas do lojista (mesma regra do
           painel manual). Se o saldo cobrir, a entrega vira `PRONTO` e o despacho pro
           entregador começa normalmente. Se não cobrir, a chamada devolve `402` e a
           entrega **continua no status anterior** (nunca fica "pendurada" em PRONTO sem
           ter cobrado) — repita a chamada depois que o lojista depositar.
        3. `{"status": "EM_ROTA"}` / `{"status": "ENTREGUE"}` — normalmente disparadas
           pelo próprio entregador (app do motoboy) ou pela mudança de status real, mas
           também podem ser chamadas por aqui se o seu fluxo controla isso.

        Chamar `PENDENTE→PRONTO` direto (pulando `EM_PREPARO`) também funciona e cobra
        do mesmo jeito — `EM_PREPARO` é só informativo, não é obrigatório.
      parameters:
        - $ref: "#/components/parameters/OrderId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [PENDENTE, EM_PREPARO, PRONTO, EM_ROTA, ENTREGUE] }
                note: { type: string }
                verificationCode: { type: string }
            examples:
              emPreparo:
                summary: "1. Avisa que começou o preparo (não cobra)"
                value: { status: "EM_PREPARO" }
              pronto:
                summary: "2. Avisa que está pronta (cobra a taxa aqui)"
                value: { status: "PRONTO" }
      responses:
        "200":
          description: Status atualizado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }
              examples:
                pronto:
                  summary: PRONTO com saldo suficiente — taxa já foi debitada
                  value:
                    id: "cmt9k2p3f0001abcd1234efgh"
                    status: "PRONTO"
                    deliveryFee: 8
                    readyAt: "2026-09-04T14:32:10.000Z"
        "400":
          description: Transição inválida, ou é uma entrega sandbox (code SANDBOX_ORDER)
        "402":
          description: Saldo insuficiente na Carteira de Entregas do lojista pra cobrir a taxa (ao tentar virar PRONTO) — a entrega NÃO muda de status, repita a chamada depois do depósito.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string }
                  code: { type: string, example: INSUFFICIENT_WALLET_BALANCE }
                  missing: { type: number, description: "Quanto falta depositar pra cobrir a taxa" }
              example:
                error: "Saldo insuficiente na Carteira de Entregas"
                code: "INSUFFICIENT_WALLET_BALANCE"
                missing: 3.5

  /orders/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Entregas]
      summary: Cancela uma entrega (não funciona em entrega sandbox — use /simulate/cancel)
      parameters:
        - $ref: "#/components/parameters/OrderId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        "200":
          description: Cancelada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Order" }

  /orders/{id}/simulate/ready:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Simula a entrega ficando pronta pra coleta"
      parameters: [{ $ref: "#/components/parameters/OrderId" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        "403": { description: Não é uma entrega sandbox (code NOT_SANDBOX_ORDER) }

  /orders/{id}/simulate/driver-accept:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Simula um entregador (fictício) aceitando"
      parameters: [{ $ref: "#/components/parameters/OrderId" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }

  /orders/{id}/simulate/collect:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Simula a coleta na loja (vira EM_ROTA)"
      parameters: [{ $ref: "#/components/parameters/OrderId" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }

  /orders/{id}/simulate/deliver:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Simula a finalização (vira ENTREGUE)"
      parameters: [{ $ref: "#/components/parameters/OrderId" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }

  /orders/{id}/simulate/insufficient-balance:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Devolve o erro de saldo insuficiente, sem mexer em nada"
      parameters: [{ $ref: "#/components/parameters/OrderId" }]
      responses:
        "402": { description: "Erro simulado, mesmo formato de uma corrida real sem saldo" }

  /orders/{id}/simulate/cancel:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Sandbox]
      summary: "[Sandbox] Simula cancelamento iniciado do nosso lado (Owner ou lojista)"
      parameters: [{ $ref: "#/components/parameters/OrderId" }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                by: { type: string, enum: [OWNER, LOJISTA], default: LOJISTA }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }

  /wallet:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    get:
      tags: [Carteira]
      summary: Consulta o saldo da Carteira de Entregas do lojista
      description: |
        Use antes de chamar `PATCH /orders/{id}/status` com `{"status": "PRONTO"}` pra
        decidir se vale a pena tentar (evita bater na cobrança só pra descobrir que vai
        voltar `402 INSUFFICIENT_WALLET_BALANCE`). `pendingBalance` é dinheiro já em
        processamento (ex: depósito Pix recém-confirmado) que ainda não caiu no saldo
        disponível pra cobrir taxa.
      responses:
        "200":
          description: Saldo atual
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance: { type: number, description: "Saldo disponível pra cobrir taxas de entrega" }
                  pendingBalance: { type: number, description: "Em processamento, ainda não disponível" }
              example:
                balance: 42.5
                pendingBalance: 0

  /wallet/deposit:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    post:
      tags: [Carteira]
      summary: Gera uma cobrança Pix pra recarregar a Carteira de Entregas do lojista
      description: |
        Mesmo mecanismo Pix (Woovi) que o lojista já usa no painel dele — o app de
        terceiro pode gerar e mostrar o QR Code pro lojista pagar sem ele precisar abrir
        o nosso painel. A confirmação do pagamento acontece de forma assíncrona: use
        `GET /wallet/deposit/{id}` pra fazer polling, ou cadastre um webhook de saída
        (evento `wallet.deposit_confirmed`) pra ser avisado assim que cair.

        **Transparência de taxa**: a resposta sempre traz os 3 números juntos —
        `value` (o Pix que o lojista efetivamente paga), `feeTotal` (taxa de depósito
        configurada) e `netAmount` (o que realmente cai disponível na Carteira). Nunca
        adivinhe `netAmount` a partir de `value` — a fórmula (fixo + percentual) é
        configurada por App/lojista e pode mudar.

        Só dispara webhook de depósito confirmado pra recargas criadas por ESSA rota —
        um depósito feito pelo lojista direto no painel dele não gera webhook (ele já
        vê a confirmação na própria tela).
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string }
          description: Repetir a mesma chave devolve o MESMO depósito já criado (200), nunca gera uma 2ª cobrança Pix.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value: { type: number, description: "Valor bruto do Pix, respeitando o mínimo/máximo configurado pro lojista" }
            example:
              value: 100
      responses:
        "201":
          description: Cobrança Pix criada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WalletDeposit" }
        "200":
          description: Já existia um depósito com essa Idempotency-Key — devolvido sem criar outra cobrança
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WalletDeposit" }
        "400":
          description: Depósito desabilitado pra essa loja, valor fora do min/max, ou Pix Online não configurado no App
        "502":
          description: Falha ao gerar a cobrança na Woovi — tente novamente

  /wallet/deposit/{id}:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    get:
      tags: [Carteira]
      summary: Consulta o status de uma recarga (poll do pagamento Pix)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Depósito encontrado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WalletDeposit" }
        "404":
          description: Não encontrado

  /delivery-fee:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    get:
      tags: [Taxas]
      summary: Resolve a taxa de entrega pra um bairro
      parameters:
        - in: query
          name: neighborhood
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Resultado da resolução (matched:false se o bairro não foi encontrado)
          content:
            application/json:
              schema:
                type: object
                properties:
                  matched: { type: boolean }
                  fee: { type: number, nullable: true }
                  zoneName: { type: string, nullable: true }
                  feeType: { type: string, enum: [FIXED, NEIGHBORHOOD], nullable: true }
                  own: { type: boolean, nullable: true }

  /delivery-zones:
    parameters:
      - $ref: "#/components/parameters/StoreCode"
    get:
      tags: [Taxas]
      summary: Lista as zonas de bairro (e taxa de cada uma) configuradas pro lojista
      description: |
        `zones` só é relevante quando `feeType=NEIGHBORHOOD` — em `FIXED` a lista pode vir vazia
        ou desatualizada (não é usada pra calcular nada), use `fixedFee` nesse caso.
      responses:
        "200":
          description: Lista de zonas
          content:
            application/json:
              schema:
                type: object
                properties:
                  own: { type: boolean, description: "true = configuração própria do lojista; false = padrão do módulo" }
                  feeType: { type: string, enum: [FIXED, NEIGHBORHOOD] }
                  fixedFee: { type: number, description: "Valor da taxa fixa — só relevante quando feeType=FIXED (sempre presente, ignore quando feeType=NEIGHBORHOOD)." }
                  zones:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        fee: { type: number }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: Lista os webhooks de saída da sua loja
      responses:
        "200":
          description: Webhooks cadastrados (inclui o secret de cada um, pra validar a assinatura)
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks: { type: array, items: { $ref: "#/components/schemas/Webhook" } }
    post:
      tags: [Webhooks]
      summary: Cadastra uma URL de webhook
      description: |
        Até 5 webhooks por loja. A URL precisa ser pública (https recomendado; endereços
        internos/privados são recusados). O `secret` devolvido assina cada evento em
        `X-Vainabag-Signature: sha256=<hmac do corpo>`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, example: "https://meusistema.com/webhooks/vainabag" }
                label: { type: string, example: "Produção" }
      responses:
        "201":
          description: Webhook criado
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook: { $ref: "#/components/schemas/Webhook" }
        "400": { description: "URL inválida ou não permitida" }
        "403": { description: "Limite de webhooks atingido (code WEBHOOK_LIMIT)" }
  /webhooks/sample-payload:
    get:
      tags: [Webhooks]
      summary: Exemplo do corpo de um evento
      responses:
        "200": { description: "Payload de exemplo de order.status_changed" }
  /webhooks/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string }
    patch:
      tags: [Webhooks]
      summary: Altera URL, nome ou liga/desliga
      description: "Reativar (`active: true`) zera o contador de falhas de um webhook desativado automaticamente."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string }
                label: { type: string }
                active: { type: boolean }
      responses:
        "200": { description: "Webhook atualizado" }
        "404": { description: "Webhook não encontrado" }
    delete:
      tags: [Webhooks]
      summary: Remove o webhook
      responses:
        "200": { description: "Removido" }
        "404": { description: "Webhook não encontrado" }
  /webhooks/{id}/test:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string }
    post:
      tags: [Webhooks]
      summary: Envia um evento de teste pra URL
      description: Dispara um evento de exemplo assinado e devolve o status HTTP que sua URL respondeu. Limite de 10 testes a cada 5 minutos.
      responses:
        "200": { description: "Resultado do envio (status da sua URL ou erro)" }
  /partner/stores:
    get:
      tags: [Parceiros]
      summary: Lojas conectadas ao parceiro (só token de parceiro)
      description: Não precisa do header X-Vainabag-Store. Só lojas com status APPROVED aceitam chamadas.
      responses:
        "200":
          description: Lojas conectadas
          content:
            application/json:
              schema:
                type: object
                properties:
                  stores:
                    type: array
                    items:
                      type: object
                      properties:
                        storeCode: { type: string, example: "vb_3f9a1c2b7d4e" }
                        storeName: { type: string }
                        city: { type: string }
                        status: { type: string, enum: [PENDING, APPROVED, PAUSED, REJECTED, INACTIVE, DISCONNECTED] }
                        connectedAt: { type: string, format: date-time }
                        approvedAt: { type: string, format: date-time, nullable: true }
        "403": { description: "Token não é de parceiro (PARTNER_ONLY)" }
  /partner/webhooks:
    get:
      tags: [Parceiros]
      summary: Lista os webhooks do parceiro
      description: "Mesmo formato de GET /webhooks. Também existem PATCH/DELETE /partner/webhooks/{id} e POST /partner/webhooks/{id}/test."
      responses:
        "200": { description: "Webhooks do parceiro" }
    post:
      tags: [Parceiros]
      summary: Cadastra uma URL que recebe os eventos de todas as lojas conectadas
      description: "Mesmo corpo de POST /webhooks. Cada evento chega com o campo storeCode indicando a loja."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string }
                label: { type: string }
      responses:
        "201": { description: "Webhook criado" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    StoreCode:
      in: header
      name: X-Vainabag-Store
      required: false
      schema: { type: string, example: "vb_713f4f517e95" }
      description: |
        **Só para parceiros** (credencial `epk_...`): o código da loja conectada a você, que o
        lojista copia no painel dele (Integrações → Parceiros). Diz de qual loja é a chamada.
        Credencial de loja (`eak_...`) ignora este header — deixe em branco.
    OrderId:
      in: path
      name: id
      required: true
      schema: { type: string }
  schemas:
    Webhook:
      type: object
      properties:
        id: { type: string }
        label: { type: string, nullable: true }
        url: { type: string }
        secret: { type: string, description: "Chave do HMAC-SHA256 de X-Vainabag-Signature" }
        active: { type: boolean }
        autoDisabledAt: { type: string, format: date-time, nullable: true, description: "Preenchido quando o webhook foi desativado sozinho depois de 20 falhas seguidas" }
        consecutiveFailures: { type: integer }
        lastTriggeredAt: { type: string, format: date-time, nullable: true }
        lastSuccessAt: { type: string, format: date-time, nullable: true }
        lastFailureAt: { type: string, format: date-time, nullable: true }
        lastResponseStatus: { type: integer, nullable: true }
        lastErrorMessage: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
    OrderCreate:
      type: object
      required: [clientName, clientPhone, address, paymentMethod]
      properties:
        test:
          type: boolean
          default: false
          description: true = entrega sandbox, isolada, nunca vai pra um entregador real.
        clientName: { type: string }
        clientPhone: { type: string }
        address:
          type: object
          required: [neighborhood]
          properties:
            street: { type: string }
            number: { type: string }
            neighborhood: { type: string, description: "Precisa bater com uma zona cadastrada (ver GET /delivery-zones)." }
            complement: { type: string }
            city: { type: string }
            latitude: { type: number }
            longitude: { type: number }
        paymentMethod: { type: string, enum: [CARTAO, DINHEIRO, PIX, PAGO] }
        amountToCollect: { type: number, description: "Obrigatório e > 0 se paymentMethod for CARTAO ou DINHEIRO." }
        changeForAmount: { type: number, description: "Só considerado se paymentMethod=DINHEIRO." }
        urgentEnabled: { type: boolean, default: false }
        bloqueioEnabled:
          type: boolean
          nullable: true
          description: |
            Override do Modo Bloqueio pra esta corrida (null = automático). SÓ tem efeito
            se o lojista tiver o Modo Bloqueio ligado nas Configurações de Entregas dele —
            senão é ignorado silenciosamente (a entrega nasce sem override, mesmo se você
            mandar true).
        refrigeranteEnabled:
          type: boolean
          nullable: true
          description: |
            Mesma regra do bloqueioEnabled acima, só que pro Modo Refrigerante do lojista —
            ignorado se ele não estiver ligado.
        status:
          type: string
          enum: [PENDENTE, EM_PREPARO, PRONTO]
          default: PENDENTE
          description: |
            Status inicial da entrega. PENDENTE (padrão) é o comportamento de sempre.
            EM_PREPARO/PRONTO já nascem adiantadas — PRONTO cobra a taxa da Carteira do
            lojista na hora (ver descrição do endpoint). Ignorado em entrega sandbox
            (test:true), que nasce sempre em PENDENTE.
        note: { type: string }
    Order:
      type: object
      properties:
        id: { type: string }
        status: { type: string, enum: [PENDENTE, EM_PREPARO, PRONTO, EM_ROTA, ENTREGUE, CANCELADO] }
        warning:
          type: object
          nullable: true
          description: |
            Só aparece em POST /orders quando status:"PRONTO" foi pedido na criação mas não
            pôde ser aplicado (ex: saldo insuficiente) — a entrega foi criada mesmo assim,
            só que em PENDENTE. Ausente/null no resto das respostas.
          properties:
            code: { type: string, example: INSUFFICIENT_WALLET_BALANCE }
            message: { type: string }
            missing: { type: number, nullable: true, description: "Valor que faltou na Carteira, quando code=INSUFFICIENT_WALLET_BALANCE." }
        origin: { type: string, example: API }
        test: { type: boolean, description: "true = entrega sandbox" }
        externalDisplayId: { type: string, nullable: true }
        storeName: { type: string, nullable: true }
        clientName: { type: string }
        clientPhone: { type: string }
        address:
          type: object
          properties:
            street: { type: string, nullable: true }
            number: { type: string, nullable: true }
            neighborhood: { type: string, nullable: true }
            complement: { type: string, nullable: true }
            city: { type: string, nullable: true }
            latitude: { type: number, nullable: true }
            longitude: { type: number, nullable: true }
        driverId: { type: string, nullable: true }
        driverName: { type: string, nullable: true }
        driverPhone: { type: string, nullable: true }
        deliveryFeeAmount: { type: number }
        extraFeesAmount: { type: number }
        paymentMethod: { type: string }
        amountToCollect: { type: number, nullable: true }
        createdAt: { type: string, format: date-time }
        readyAt: { type: string, format: date-time, nullable: true }
        collectedAt: { type: string, format: date-time, nullable: true }
        deliveredAt: { type: string, format: date-time, nullable: true }
        canceledAt: { type: string, format: date-time, nullable: true }
        cancelReason: { type: string, nullable: true }

    WalletDeposit:
      type: object
      properties:
        depositId: { type: string }
        status: { type: string, enum: [PENDING, PAID, EXPIRED, CANCELLED] }
        value: { type: number, description: "Valor bruto do Pix pago pelo lojista" }
        feeTotal: { type: number, description: "Taxa de depósito descontada" }
        netAmount: { type: number, description: "O que efetivamente cai disponível na Carteira (value - feeTotal)" }
        qrCodeImage: { type: string, nullable: true, description: "PNG em base64 do QR Code Pix" }
        brCode: { type: string, nullable: true, description: "Copia-e-cola Pix" }
        expiresAt: { type: string, format: date-time }
        paidAt: { type: string, format: date-time, nullable: true }
