# BitPay API v1 — contrato OpenAPI (SDD §10, Anexos A/B)
# Implementação de referência: services/bitpay-sandbox (ambiente de testes).
# O core financeiro (Java 21 + Spring Boot 3) implementa ESTE contrato.
# Cópias publicadas: https://developers.bitpay.ao/openapi.yaml e /openapi.json
openapi: 3.1.0
info:
  title: BitPay API
  version: "1.0.0-sandbox"
  summary: Gateway de pagamentos para Angola — Multicaixa Express e Referências Multicaixa.
  description: |
    Uma integração. Todos os pagamentos de Angola.

    **Estado:** a BitPay ainda **não está em produção** — aguarda a certificação da EMIS.
    Só o **sandbox** está disponível: nenhum pagamento é real e nenhum dinheiro é movimentado.

    ## Começar em 1 minuto
    1. `POST /sandbox/accounts` (sem autenticação) → devolve `sk_test_…` e `pk_test_…`.
       Para usar também o painel, cria antes a conta em https://dashboard.bitpay.ao e
       copia a `sk_test_` em Developers → Chaves (as contas criadas por API não têm login).
    2. Autentica todos os pedidos com `Authorization: Bearer sk_test_…`.
    3. Envia sempre `Content-Type: application/json` nos pedidos com corpo.
    4. `POST /payment_intents` exige `Idempotency-Key` (8–128 caracteres).

    ## Números de teste (Multicaixa Express, `customer.mobile`)
    | Número | Resultado |
    |---|---|
    | 923000000 | PENDING → PROCESSING (~2 s) → SUCCEEDED (~5 s) |
    | 923000001 | PROCESSING → FAILED `rejected_by_customer` (~5 s) |
    | 923000002 | PROCESSING → UNKNOWN (~10 s) → SUCCEEDED (~30 s, reconciliação) |
    | 923000003 | PROCESSING → FAILED `insufficient_funds` (~5 s) |
    | 923000004 | FAILED `provider_unavailable` (~2 s) |
    | qualquer outro 9XXXXXXXX | SUCCEEDED |

    Referências Multicaixa ficam `PENDING` até serem pagas — no sandbox paga-se com
    `POST /sandbox/simulate/reference_paid`. Expiram ao fim de 72 h (`EXPIRED`).

    ## Webhooks
    Cada evento é enviado por `POST` JSON ao teu endpoint com os headers
    `BitPay-Signature: t=<unix>,v1=<hex>` e `BitPay-Event-Id`. `v1` =
    HMAC-SHA256(`<t>.<corpo cru>`, `whsec_…`). Rejeita se |agora − t| > 600 s.
    Entrega at-least-once: retries 10 s / 60 s / 5 min, depois `DEAD_LETTER`.
    **Entrega a encomenda uma única vez por `data.id`** — o mesmo pagamento pode
    chegar mais de uma vez.

    ## Limites
    120 pedidos/minuto por chave. Todas as respostas trazem `X-RateLimit-Limit`,
    `X-RateLimit-Remaining` e `X-RateLimit-Reset`; um `429` traz `Retry-After` (segundos).
    Para acompanhar pagamentos prefere webhooks ou o stream SSE a polling apertado
    (se fizeres polling, 1 pedido a cada 2 s por pagamento chega).
  contact:
    name: BitPay Developers
    email: developers@bitpay.ao
    url: https://developers.bitpay.ao
servers:
  - url: https://api-sandbox.bitpay.ao/v1
    description: Sandbox (determinístico, sem pagamentos reais) — o único disponível hoje
  - url: https://api.bitpay.ao/v1
    description: Produção — ainda indisponível (aguarda certificação EMIS)

security:
  - bearerAuth: []

tags:
  - name: Sandbox
    description: Conta de testes e simulações (só sandbox).
  - name: Payment intents
    description: Cobranças Multicaixa Express e Referências Multicaixa.
  - name: Refunds
    description: Reembolsos totais ou parciais.
  - name: Payment methods
    description: Métodos disponíveis e limites de montante.
  - name: Payment links
    description: Links de pagamento partilháveis (pay.bitpay.ao).
  - name: Checkout
    description: Checkout hospedado (checkout.bitpay.ao).
  - name: QR codes
    description: Códigos QR que apontam para links de pagamento.
  - name: Webhooks
    description: Endpoints, eventos de teste e histórico de entregas.
  - name: Events & logs
    description: Eventos emitidos e pedidos feitos à API.

paths:
  /sandbox/accounts:
    post:
      tags: [Sandbox]
      operationId: createSandboxAccount
      summary: Criar conta sandbox (sem autenticação)
      security: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 80, example: Minha Loja }
                email: { type: string, format: email, description: Opcional — recebe o email de boas-vindas. }
      responses:
        "201":
          description: Conta criada. Guarda a `sk` — é a tua chave secreta.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SandboxAccount" }
              example:
                object: sandbox_account
                id: acct_2f1c0d9e8b7a6c5d4e3f2a1b
                name: Minha Loja
                email: null
                pk: pk_test_9c47f3…
                sk: sk_test_8a0670…
                livemode: false
                created_at: "2026-09-27T12:00:00.000Z"
        "400": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /sandbox/simulate/reference_paid:
    post:
      tags: [Sandbox]
      operationId: simulateReferencePaid
      summary: Pagar uma Referência Multicaixa (só sandbox)
      description: Simula o cliente a pagar a referência no ATM/app. Emite `reference.paid` e `payment.succeeded`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reference]
              properties:
                reference: { type: string, description: "`reference.number` do payment intent (9 dígitos).", example: "221610687" }
      responses:
        "200":
          description: Referência paga — devolve o payment intent atualizado (`SUCCEEDED`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentIntent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /payment_intents:
    post:
      tags: [Payment intents]
      operationId: createPaymentIntent
      summary: Criar pagamento
      description: |
        Cria uma cobrança. `Idempotency-Key` é obrigatório: repetir o pedido com a mesma
        chave e o mesmo corpo devolve a resposta original (mesmo status, header
        `Idempotency-Replayed: true`) sem criar outro pagamento; a mesma chave com outro
        corpo dá `409 idempotency_conflict` (a ordem das chaves do JSON não conta).
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaymentIntentCreate" }
            examples:
              express:
                summary: Multicaixa Express
                value: { amount: 25000, currency: AOA, payment_method: multicaixa_express, customer: { mobile: "923000000" }, merchant_reference: ENC-1001 }
              reference:
                summary: Referência Multicaixa
                value: { amount: 15000, currency: AOA, payment_method: multicaixa_reference, merchant_reference: ENC-1002 }
      responses:
        "201":
          description: Criado (e também numa repetição idempotente).
          headers:
            Idempotency-Replayed: { schema: { type: string, enum: ["true"] }, description: Presente quando a resposta é uma repetição. }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentIntent" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/Maintenance" }
    get:
      tags: [Payment intents]
      operationId: listPaymentIntents
      summary: Listar pagamentos (mais recentes primeiro)
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 25 } }
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/PaymentIntent" } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /payment_intents/{id}:
    get:
      tags: [Payment intents]
      operationId: getPaymentIntent
      summary: Obter pagamento
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentIntent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /payment_intents/{id}/cancel:
    post:
      tags: [Payment intents]
      operationId: cancelPaymentIntent
      summary: Cancelar pagamento
      description: Só em `PENDING` ou `PROCESSING`. Emite `payment.cancelled`.
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: Cancelado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentIntent" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /payment_intents/{id}/events:
    get:
      tags: [Payment intents]
      operationId: streamPaymentIntent
      summary: Acompanhar um pagamento em tempo real (SSE)
      description: |
        Server-Sent Events. Primeiro chega um evento `snapshot` com o estado atual; depois
        um evento por cada mudança (`payment.succeeded`, `payment.failed`, …). Fecha quando
        o pagamento chega a um estado final.
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: Stream `text/event-stream`
          content:
            text/event-stream:
              schema: { type: string }
              example: |
                event: snapshot
                data: {"id":"pay_…","status":"PROCESSING",…}

                event: payment.succeeded
                data: {"id":"pay_…","status":"SUCCEEDED",…}
        "404": { $ref: "#/components/responses/NotFound" }

  /refunds:
    post:
      tags: [Refunds]
      operationId: createRefund
      summary: Reembolsar (total ou parcial)
      description: |
        Só para pagamentos `SUCCEEDED`/`PARTIALLY_REFUNDED`. Sem `amount` reembolsa o que resta.
        Fica `PENDING` e passa a `SUCCEEDED` em ~3 s (evento `refund.succeeded`).
        `Idempotency-Key` obrigatório.
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payment_intent]
              properties:
                payment_intent: { type: string, example: pay_3f2a… }
                amount: { type: integer, minimum: 1, description: Em AOA. Omitido = restante. }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Refund" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /refunds/{id}:
    get:
      tags: [Refunds]
      operationId: getRefund
      summary: Obter reembolso
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Refund" }
        "404": { $ref: "#/components/responses/NotFound" }

  /payment_methods:
    get:
      tags: [Payment methods]
      operationId: listPaymentMethods
      summary: Métodos disponíveis e limites
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/PaymentMethod" } }

        "401": { $ref: "#/components/responses/Unauthorized" }
  /payment_links:
    post:
      tags: [Payment links]
      operationId: createPaymentLink
      summary: Criar link de pagamento
      description: O cliente abre `url` (pay.bitpay.ao/CODE) e paga. No sandbox paga-se lá com o número 923000000.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: integer, minimum: 1, example: 12000 }
                description: { type: string, maxLength: 140, example: Encomenda 1001 }
                methods: { type: array, items: { $ref: "#/components/schemas/MethodCode" }, description: Omitido = todos os métodos ativos. }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentLink" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [Payment links]
      operationId: listPaymentLinks
      summary: Listar links (últimos 100)
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/PaymentLink" } }

        "401": { $ref: "#/components/responses/Unauthorized" }
  /payment_links/{id}:
    get:
      tags: [Payment links]
      operationId: getPaymentLink
      summary: Obter link
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentLink" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Payment links]
      operationId: deletePaymentLink
      summary: Desativar link
      description: |
        Desativação (soft delete): devolve o link com `active: false`. A página pública deixa
        de aceitar pagamentos (`410 payment_link_inactive`); pagamentos antigos mantêm-se.
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: Desativado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaymentLink" }
        "404": { $ref: "#/components/responses/NotFound" }

  /payment_links/{id}/qr:
    get:
      tags: [Payment links]
      operationId: getPaymentLinkQr
      summary: QR do link (SVG 512 px)
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: Imagem SVG
          content:
            image/svg+xml:
              schema: { type: string }
        "404": { $ref: "#/components/responses/NotFound" }

  /qr_codes:
    post:
      tags: [QR codes]
      operationId: createQrCode
      summary: Criar QR de pagamento
      description: Aponta para um link existente (`payment_link` = código) ou cria um link novo a partir de `amount`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                amount: { type: integer, minimum: 1, example: 5000 }
                description: { type: string, maxLength: 140 }
                payment_link: { type: string, description: Código de um link existente (ex. 5D2001). }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QrCode" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /checkout/sessions:
    post:
      tags: [Checkout]
      operationId: createCheckoutSession
      summary: Criar sessão de checkout hospedado
      description: |
        Redireciona o cliente para `url` (checkout.bitpay.ao/cs_…). Quando paga, a sessão passa a
        `COMPLETED`, ganha `payment_intent` e é emitido `checkout.session.completed` (além dos
        eventos `payment.*` do pagamento, que trazem o teu `merchant_reference`). Expira em 30 min.
        No sandbox paga-se na página com o número 923000000.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: integer, minimum: 1, example: 18500 }
                merchant_reference: { type: string, example: ENC-1003 }
                success_url: { type: string, format: uri, example: "https://a-tua-loja.ao/obrigado" }
                cancel_url: { type: string, format: uri, example: "https://a-tua-loja.ao/carrinho" }
                methods: { type: array, items: { $ref: "#/components/schemas/MethodCode" } }
      responses:
        "201":
          description: Criada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutSession" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /checkout/sessions/{id}:
    get:
      tags: [Checkout]
      operationId: getCheckoutSession
      summary: Obter sessão de checkout
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CheckoutSession" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhook_endpoints:
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Registar endpoint de webhooks
      description: O `secret` (whsec_…) só é mostrado nesta resposta — guarda-o.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri, example: "https://a-tua-loja.ao/webhooks/bitpay" }
                events:
                  type: array
                  description: 'Tipos de evento a receber. Omitido ou `["*"]` = todos.'
                  items: { oneOf: [{ $ref: "#/components/schemas/EventType" }, { type: string, const: "*" }] }
                name: { type: string, maxLength: 80 }
                description: { type: string, maxLength: 200 }
      responses:
        "201":
          description: Criado
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookEndpoint"
                  - type: object
                    properties:
                      secret: { type: string, example: whsec_5411d6… }
        "400": { $ref: "#/components/responses/BadRequest" }
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: Listar endpoints
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/WebhookEndpoint" } }

        "401": { $ref: "#/components/responses/Unauthorized" }
  /webhook_endpoints/{id}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Apagar endpoint (e as suas entregas)
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: Apagado
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  object: { type: string, const: webhook_endpoint }
                  deleted: { type: boolean, const: true }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhook_endpoints/{id}/test:
    post:
      tags: [Webhooks]
      operationId: testWebhookEndpoint
      summary: Enviar evento de teste (test.ping)
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "202":
          description: Em fila
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: test_event }
                  event_id: { type: string }
                  state: { type: string, const: QUEUED }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhook_endpoints/{id}/deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Histórico de entregas (últimas 50)
      description: Inclui o corpo e o header de assinatura exatos da última tentativa — usa-os para testar a tua verificação.
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/WebhookDelivery" } }
        "404": { $ref: "#/components/responses/NotFound" }

  /events:
    get:
      tags: [Events & logs]
      operationId: listEvents
      summary: Eventos da conta (mais recentes primeiro)
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/Event" } }

        "401": { $ref: "#/components/responses/Unauthorized" }
  /logs:
    get:
      tags: [Events & logs]
      operationId: listLogs
      summary: Pedidos à API feitos com a tua chave (diagnóstico)
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
      responses:
        "200":
          description: Lista
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        request_id: { type: string }
                        method: { type: string }
                        path: { type: string }
                        status: { type: integer }
                        error_code: { type: [string, "null"] }
                        latency_ms: { type: integer }
                        created_at: { type: string, format: date-time }

        "401": { $ref: "#/components/responses/Unauthorized" }
webhooks:
  event:
    post:
      operationId: receiveEvent
      summary: Evento enviado ao teu endpoint
      description: Verifica `BitPay-Signature` antes de confiar no corpo; responde 2xx em < 5 s.
      parameters:
        - { name: BitPay-Signature, in: header, required: true, schema: { type: string, example: "t=1790000000,v1=5f2b…" } }
        - { name: BitPay-Event-Id, in: header, required: true, schema: { type: string } }
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Event" }
      responses:
        "200": { description: Recebido }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "`Authorization: Bearer sk_test_…` (sandbox) — nunca exponhas a sk_ no browser ou numa app."

  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 8–128 caracteres, único por operação (ex. o id da tua encomenda). Válido para sempre na conta.
      schema: { type: string, minLength: 8, maxLength: 128, example: encomenda-1001 }

  headers:
    X-RateLimit-Limit: { schema: { type: integer }, description: Pedidos permitidos por minuto (120). }
    X-RateLimit-Remaining: { schema: { type: integer } }
    X-RateLimit-Reset: { schema: { type: integer }, description: Unix time (s) em que a janela reinicia. }
    Retry-After: { schema: { type: integer }, description: Segundos a esperar antes de repetir. }

  responses:
    BadRequest:
      description: "Pedido inválido — `invalid_request`, `invalid_json`, `amount_out_of_range`, `invalid_phone_number`, `method_disabled`."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: "`authentication_failed` — chave em falta ou inválida."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: "`permission_denied`, `feature_disabled`, `account_suspended`, `email_not_verified`."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: "`resource_not_found`."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Conflict:
      description: "`idempotency_conflict` ou `payment_already_processed`."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    TooManyRequests:
      description: "`rate_limit_exceeded` — espera `Retry-After` segundos."
      headers:
        Retry-After: { $ref: "#/components/headers/Retry-After" }
        X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" }
        X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" }
        X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" }
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Maintenance:
      description: "`maintenance` — plataforma em manutenção programada; repete mais tarde."
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Código estável — usa-o no teu código (a mensagem pode mudar).
              examples: [invalid_request, invalid_json, amount_out_of_range, invalid_phone_number, method_disabled, authentication_failed, permission_denied, feature_disabled, resource_not_found, idempotency_conflict, payment_already_processed, payment_link_inactive, payload_too_large, rate_limit_exceeded, maintenance, internal_error]
            message: { type: string }
            request_id: { type: string, description: Inclui-o quando contactares o suporte. }
      example:
        error: { code: amount_out_of_range, message: "'amount' abaixo do mínimo de 100 AOA para Multicaixa Express.", request_id: req_8f3a… }

    SandboxAccount:
      type: object
      properties:
        object: { type: string, const: sandbox_account }
        id: { type: string }
        name: { type: string }
        email: { type: [string, "null"] }
        pk: { type: string }
        sk: { type: string }
        livemode: { type: boolean, const: false }
        created_at: { type: string, format: date-time }

    MethodCode:
      type: string
      enum: [multicaixa_express, multicaixa_reference]
      description: Métodos ativos hoje. `unitel_money`, `kwik` e `direct_debit` estão em "coming_soon" (ver /payment_methods).

    PaymentIntentCreate:
      type: object
      required: [amount, currency, payment_method]
      properties:
        amount: { type: integer, minimum: 1, description: "Inteiro em AOA (sem cêntimos). Mínimo 100 e máximo por método — ver /payment_methods.", example: 25000 }
        currency: { type: string, const: AOA }
        payment_method: { $ref: "#/components/schemas/MethodCode" }
        customer:
          type: object
          description: Obrigatório em multicaixa_express.
          properties:
            mobile: { type: string, pattern: "^9\\d{8}$", example: "923000000" }
        merchant_reference: { type: string, maxLength: 64, description: O teu id da encomenda — volta em todos os eventos. }
        metadata: { type: object, additionalProperties: true }

    PaymentStatus:
      type: string
      enum: [PENDING, PROCESSING, UNKNOWN, SUCCEEDED, FAILED, CANCELLED, EXPIRED, PARTIALLY_REFUNDED, REFUNDED]
      description: |
        PENDING → PROCESSING → SUCCEEDED | FAILED. Em timeout do operador: UNKNOWN (nunca
        assumas falha) → resolvido por reconciliação (normalmente SUCCEEDED). Só SUCCEEDED,
        PARTIALLY_REFUNDED e REFUNDED significam "pago".

    PaymentIntent:
      type: object
      properties:
        id: { type: string, example: pay_3f2a1b… }
        object: { type: string, const: payment_intent }
        amount: { type: integer }
        currency: { type: string, const: AOA }
        payment_method: { $ref: "#/components/schemas/MethodCode" }
        status: { $ref: "#/components/schemas/PaymentStatus" }
        failure_code:
          type: [string, "null"]
          enum: [rejected_by_customer, insufficient_funds, provider_unavailable, null]
        customer:
          type: [object, "null"]
          properties:
            mobile: { type: string }
        merchant_reference: { type: [string, "null"] }
        reference:
          type: [object, "null"]
          description: Só em multicaixa_reference — mostra estes dados ao cliente.
          properties:
            entity: { type: string, example: "90201" }
            number: { type: string, example: "221610687" }
            expires_at: { type: string, format: date-time, description: 72 h após a criação. }
        amount_refunded: { type: integer }
        fee: { type: [integer, "null"], description: Comissão BitPay em AOA. }
        fee_version: { type: [integer, "null"], description: Versão da regra de comissão aplicada. }
        api_key_id: { type: [string, "null"] }
        metadata: { type: object }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    Refund:
      type: object
      properties:
        id: { type: string, example: rf_… }
        object: { type: string, const: refund }
        payment_intent: { type: string }
        amount: { type: integer }
        status: { type: string, enum: [PENDING, SUCCEEDED] }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    PaymentMethod:
      type: object
      properties:
        code: { type: string }
        display_name: { type: string }
        status: { type: string, enum: [active, coming_soon] }
        min_amount: { type: [integer, "null"] }
        max_amount: { type: [integer, "null"] }
        channels:
          type: object
          properties:
            api: { type: boolean }
            checkout: { type: boolean }
            link: { type: boolean }

    PaymentLink:
      type: object
      properties:
        id: { type: string, example: plink_… }
        object: { type: string, const: payment_link }
        code: { type: string, example: 5D2001 }
        url: { type: string, format: uri, example: "https://pay.bitpay.ao/5D2001" }
        amount: { type: integer }
        currency: { type: string, const: AOA }
        methods: { type: array, items: { $ref: "#/components/schemas/MethodCode" } }
        description: { type: [string, "null"] }
        active: { type: boolean }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    QrCode:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: qr_code }
        payment_link: { type: string, description: Código do link para onde o QR aponta. }
        url: { type: string, format: uri }
        svg: { type: string, description: Imagem SVG inline (512 px). }
        livemode: { type: boolean }

    CheckoutSession:
      type: object
      properties:
        id: { type: string, example: cs_8ff4… }
        object: { type: string, const: checkout_session }
        url: { type: string, format: uri, example: "https://checkout.bitpay.ao/cs_8ff4bc5ef2237b389dc57eac" }
        amount: { type: integer }
        currency: { type: string, const: AOA }
        methods: { type: array, items: { $ref: "#/components/schemas/MethodCode" } }
        status: { type: string, enum: [OPEN, COMPLETED, EXPIRED] }
        payment_intent: { type: [string, "null"], description: Preenchido quando o cliente paga. }
        merchant_reference: { type: [string, "null"] }
        success_url: { type: [string, "null"] }
        cancel_url: { type: [string, "null"] }
        expires_at: { type: string, format: date-time }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }

    WebhookEndpoint:
      type: object
      properties:
        id: { type: string, example: whe_… }
        object: { type: string, const: webhook_endpoint }
        url: { type: string, format: uri }
        events: { type: array, items: { type: string } }
        name: { type: [string, "null"] }
        description: { type: [string, "null"] }
        created_at: { type: string, format: date-time }

    WebhookDelivery:
      type: object
      properties:
        id: { type: string }
        object: { type: string, const: webhook_delivery }
        event_id: { type: string }
        event_type: { $ref: "#/components/schemas/EventType" }
        state: { type: string, enum: [PENDING, RETRYING, DELIVERED, DEAD_LETTER] }
        attempts: { type: integer }
        last_status: { type: [integer, "null"], description: HTTP devolvido pelo teu servidor (0 = sem resposta). }
        last_ms: { type: [integer, "null"] }
        last_attempt_at: { type: [string, "null"], format: date-time }
        request_body: { type: [string, "null"], description: Corpo cru exato enviado na última tentativa. }
        signature_header: { type: [string, "null"], description: Header BitPay-Signature dessa tentativa. }

    EventType:
      type: string
      enum:
        - payment.created
        - payment.succeeded
        - payment.failed
        - payment.unknown
        - payment.reconciled
        - payment.expired
        - payment.cancelled
        - reference.paid
        - refund.succeeded
        - checkout.session.completed
        - test.ping
      description: |
        - `payment.succeeded` — **o sinal para entregar a encomenda** (uma vez por pagamento).
        - `payment.reconciled` — chega DEPOIS de `payment.succeeded` quando o pagamento esteve em
          UNKNOWN; é informativo, não voltes a entregar.
        - `reference.paid` — chega antes de `payment.succeeded` nas referências.
        - `payment.expired` — referência não paga em 72 h.
        - `checkout.session.completed` — `data` é a CheckoutSession (com `payment_intent`).
        - `refund.succeeded` — `data` é o Refund.
        - `test.ping` — enviado por /webhook_endpoints/{id}/test.

    Event:
      type: object
      properties:
        id: { type: string, example: evt_… }
        object: { type: string, const: event }
        type: { $ref: "#/components/schemas/EventType" }
        livemode: { type: boolean }
        created_at: { type: string, format: date-time }
        data:
          description: PaymentIntent (payment.*, reference.paid), Refund, CheckoutSession ou {message} (test.ping).
          anyOf:
            - $ref: "#/components/schemas/PaymentIntent"
            - $ref: "#/components/schemas/Refund"
            - $ref: "#/components/schemas/CheckoutSession"
            - type: object
              properties:
                message: { type: string }
