# Fuente de verdad del API publico /v1 (servicio payment-api, api.apipay.io).
# Cualquier cambio pasa por PR: se valida con Spectral (contracts/.spectral.yaml)
# y con oasdiff contra la version de main (breaking change = PR bloqueado).
openapi: 3.0.3
info:
  title: ApiPay Hub - Payment API
  version: 1.0.0
  description: |
    API publica de pagos de ApiPay Hub (servicio payment-api).
    Montos en unidades menores (amount_minor) + currency ISO 4217.
    Errores RFC 9457 (application/problem+json). Idempotencia via header Idempotency-Key.
    Timestamps ISO 8601 UTC.

    Convenciones transversales:
      - Paths en kebab-case, campos JSON en snake_case, enums de estado en MAYUSCULAS.
      - Identificadores publicos opacos y prefijados: pi_ (payment intent), re_ (refund),
        txn_ (transaction), evt_ (evento), whd_ (delivery). Nunca se exponen PKs internas.
      - Paginacion por cursor (cursor + limit), nunca por offset.
      - Toda respuesta incluye el header X-Request-Id para correlacion en soporte y
        propaga el contexto de traza W3C (traceparent).
      - El client_secret viaja SIEMPRE en el header X-Client-Secret, jamas como query param.
  contact:
    name: ApiPay Platform Team
    url: https://admin.apipay.io
    email: platform@apipay.io
  license:
    name: ApiPay Hub API Terms
    url: https://apipay.io/legal/api-terms
servers:
  - url: https://api.apipay.io
    description: Produccion
security:
  - ApiKeyAuth: []
tags:
  - name: payment-intents
    description: Ciclo de vida del payment intent
  - name: refunds
    description: Reembolsos totales y parciales
  - name: transactions
    description: Consulta de transacciones
  - name: payment-methods
    description: Medios de pago guardados e inscripcion de tarjetas
  - name: gateway-webhooks
    description: Webhooks entrantes de pasarelas

paths:
  /v1/payment-intents:
    post:
      tags: [payment-intents]
      operationId: createPaymentIntent
      summary: Crear un payment intent
      description: >
        Crea un payment intent en estado CREATED. Requiere clave secreta (sk_) y
        header Idempotency-Key; repetir la misma clave devuelve la respuesta original.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePaymentIntentRequest'
            examples:
              webpayClp:
                summary: Pago Webpay Plus en CLP
                value:
                  amount_minor: 1499000
                  currency: CLP
                  gateway_id: webpay
                  description: "Orden 4831 - Tienda Andina"
                  customer_email: "cliente@example.com"
                  return_url: "https://tienda-andina.cl/checkout/retorno"
                  metadata:
                    order_id: "4831"
              sinPasarelaExplicita:
                summary: Pago en USD sin gateway_id (lo resuelve el enrutado del tenant)
                value:
                  amount_minor: 1050
                  currency: USD
                  description: "Suscripcion mensual"
                  customer_email: "cliente@example.com"
      responses:
        '201':
          description: Payment intent creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntent'
              examples:
                created:
                  summary: Intent recien creado
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: CREATED
                    client_secret: pi_9f2c4a7d1b3e4f60a8c5d2e7_secret_Vt8yQ1
                    gateway_id: webpay
                    payment_method: null
                    redirect_url: null
                    metadata:
                      order_id: "4831"
                    livemode: true
                    created_at: "2026-08-06T14:30:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/GatewayUnavailable'

  /v1/payment-intents/{id}:
    get:
      tags: [payment-intents]
      operationId: getPaymentIntent
      summary: Obtener un payment intent
      description: >
        Con clave sk_ devuelve el recurso completo. Con clave pk_ es obligatorio
        el header X-Client-Secret y la respuesta omite campos server-only e
        incluye el bloque checkout (schema CheckoutConfig).
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del payment intent (prefijo pi_)
          schema:
            type: string
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        - name: X-Client-Secret
          in: header
          required: false
          description: >
            client_secret del payment intent. Obligatorio cuando se autentica
            con clave publicable pk_; nunca viaja como query param.
          schema:
            type: string
      responses:
        '200':
          description: Payment intent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntent'
              examples:
                withSecretKey:
                  summary: Lectura server-to-server con sk_
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: SUCCEEDED
                    client_secret: pi_9f2c4a7d1b3e4f60a8c5d2e7_secret_Vt8yQ1
                    gateway_id: webpay
                    payment_method:
                      type: card
                      brand: visa
                      last4: "4242"
                      exp_month: 11
                      exp_year: 2030
                    redirect_url: null
                    metadata:
                      order_id: "4831"
                    livemode: true
                    created_at: "2026-08-06T14:30:00Z"
                withPublishableKey:
                  summary: Lectura desde el checkout-widget con pk_ + X-Client-Secret
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: CREATED
                    gateway_id: webpay
                    payment_method: null
                    redirect_url: null
                    metadata: {}
                    livemode: true
                    created_at: "2026-08-06T14:30:00Z"
                    checkout:
                      tenant_name: Tienda Andina
                      logo_url: "https://cdn.apipay.io/tenants/tienda-andina/logo.svg"
                      allowed_origins: ["https://tienda-andina.cl"]
                      payment_methods:
                        - gateway_id: webpay
                          public_config:
                            commerce_display_name: "Tienda Andina"
                      theme:
                        accent_color: "#0F62FE"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/payment-intents/{id}/confirm:
    post:
      tags: [payment-intents]
      operationId: confirmPaymentIntent
      summary: Confirmar un payment intent
      description: >
        Inicia el cobro en la pasarela via el SPI PaymentGateway. Puede devolver
        REQUIRES_ACTION con redirect_url (flujos de redireccion como Webpay Plus),
        PROCESSING o SUCCEEDED segun la pasarela. Acepta dos formas de
        autenticacion: clave sk_ (server-to-server) o clave pk_ + header
        X-Client-Secret (sesion de checkout del widget, p. ej. el segundo
        confirm que committea el token_ws de Webpay).
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del payment intent (prefijo pi_)
          schema:
            type: string
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        - name: X-Client-Secret
          in: header
          required: false
          description: >
            client_secret del payment intent. Obligatorio cuando se autentica
            con clave publicable pk_ (sesion de checkout del widget).
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmPaymentIntentRequest'
            examples:
              hostedFieldsToken:
                summary: Confirmacion con token de hosted fields
                value:
                  payment_method_token: tok_test_1PxYzA2b
                  return_url: "https://tienda-andina.cl/checkout/retorno"
              redirectFlow:
                summary: Confirmacion de un flujo de redireccion (cuerpo vacio)
                value: {}
      responses:
        '200':
          description: Payment intent actualizado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntent'
              examples:
                requiresAction:
                  summary: Redireccion a la pasarela
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: REQUIRES_ACTION
                    client_secret: pi_9f2c4a7d1b3e4f60a8c5d2e7_secret_Vt8yQ1
                    gateway_id: webpay
                    payment_method: null
                    redirect_url: "https://webpay3g.transbank.cl/webpayserver/initTransaction?token=e9d0f1"
                    metadata:
                      order_id: "4831"
                    livemode: true
                    created_at: "2026-08-06T14:30:00Z"
                succeeded:
                  summary: Cobro aprobado en linea
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: SUCCEEDED
                    client_secret: pi_9f2c4a7d1b3e4f60a8c5d2e7_secret_Vt8yQ1
                    gateway_id: webpay
                    payment_method:
                      type: card
                      brand: visa
                      last4: "4242"
                      exp_month: 11
                      exp_year: 2030
                    redirect_url: null
                    metadata:
                      order_id: "4831"
                    livemode: true
                    created_at: "2026-08-06T14:30:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/GatewayUnavailable'

  /v1/payment-intents/{id}/cancel:
    post:
      tags: [payment-intents]
      operationId: cancelPaymentIntent
      summary: Cancelar un payment intent
      description: >
        Solo valido en estados CREATED y REQUIRES_ACTION; deja el intent en CANCELED.
        Requiere clave secreta (sk_). Cancelar un intent ya terminal responde
        409 invalid_state_transition.
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del payment intent (prefijo pi_)
          schema:
            type: string
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelPaymentIntentRequest'
            example:
              reason: requested_by_customer
      responses:
        '200':
          description: Payment intent cancelado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntent'
              examples:
                canceled:
                  summary: Intent cancelado por el comercio
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: CANCELED
                    client_secret: pi_9f2c4a7d1b3e4f60a8c5d2e7_secret_Vt8yQ1
                    gateway_id: webpay
                    payment_method: null
                    redirect_url: null
                    metadata:
                      order_id: "4831"
                    livemode: true
                    created_at: "2026-08-06T14:30:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/payment-intents/{id}/inscriptions:
    post:
      tags: [payment-intents, payment-methods]
      operationId: openPaymentIntentInscription
      summary: Inscribir una tarjeta para este payment intent
      description: >
        Abre la inscripcion de una tarjeta PARA ESTE INTENT y devuelve el formulario de la
        pasarela al que hay que enviar al titular. Es lo que permite pagar con una tarjeta
        guardada (Oneclick) desde el checkout alojado, sin salir del flujo.

        Acepta las dos formas de autenticacion del checkout: clave sk_ o clave pk_ + header
        X-Client-Secret. La pk_ no gana poderes generales con esto: solo puede inscribir para
        el intent cuyo client_secret presenta, que es el mismo alcance que ya tenia con el
        confirm. Guardar un medio de pago desatendido sigue siendo POST
        /v1/payment-methods/inscriptions, solo con sk_.

        El customer_ref NO es un campo de entrada: se deriva del intent
        (metadata.customer_ref si el comercio lo puso, y si no el id publico del intent), para
        que una pagina no pueda inscribir una tarjeta a nombre de otro cliente del comercio.
        El email y la return_url tambien se derivan del intent cuando el cuerpo no los trae,
        asi que el cuerpo entero es opcional.

        Aqui NO viaja ningun dato de tarjeta: el PAN se teclea en el formulario de la pasarela
        (PCI DSS SAQ A).

        Requiere Idempotency-Key: cada apertura crea un token en la pasarela y un reintento sin
        clave dejaria el anterior huerfano. La clave se acota al intent, de modo que repetirla
        en otro intent no devuelve esta inscripcion.
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del payment intent (prefijo pi_)
          schema:
            type: string
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: X-Client-Secret
          in: header
          required: false
          description: >
            client_secret del payment intent. Obligatorio cuando se autentica con clave
            publicable pk_ (sesion de checkout del widget).
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OpenPaymentIntentInscriptionRequest'
            examples:
              oneclickDesdeElCheckout:
                summary: El comprador elige Oneclick en el selector del widget
                value:
                  gateway_id: oneclick
                  email: "cliente@example.com"
                  return_url: "https://checkout.apipay.io/session"
              todoDerivadoDelIntent:
                summary: Cuerpo vacio; pasarela, correo y retorno salen del intent
                value: {}
      responses:
        '201':
          description: >
            Inscripcion abierta; redirect dice como llevar al titular a la pasarela. La
            pasarela del intent NO queda fijada todavia: se fija al cerrar la inscripcion, para
            que un comprador que vuelva atras siga viendo el selector completo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethodInscription'
              examples:
                abierta:
                  summary: Inscripcion recien abierta en Transbank Oneclick
                  value:
                    id: ins_Kw3pR8sYt5Nc2Hj7QmZxVb
                    object: payment_method_inscription
                    status: REQUIRES_ACTION
                    gateway_id: oneclick
                    customer_ref: "cus_42"
                    expires_at: "2026-08-13T10:30:00Z"
                    created_at: "2026-08-13T10:15:00Z"
                    redirect:
                      method: POST
                      url: "https://webpay3gint.transbank.cl/webpayserver/bp_multicode_inscription.cgi"
                      field_name: TBK_TOKEN
                      token: "01ab23cd45ef67"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/GatewayUnavailable'

  /v1/payment-intents/{id}/inscriptions/{inscriptionId}/confirm:
    post:
      tags: [payment-intents, payment-methods]
      operationId: confirmPaymentIntentInscription
      summary: Cerrar la inscripcion y dejar el intent cobrable con esa tarjeta
      description: >
        Cierra la inscripcion contra la pasarela y ASOCIA la tarjeta resultante al intent, en
        el servidor. Devuelve el payment intent, ya con su medio de pago y con la pasarela de
        la tarjeta fijada: despues se cobra con el confirm de siempre, sin cuerpo nuevo.

        Devuelve el intent y no el medio de pago a proposito: el identificador del medio (pm_)
        no llega nunca al navegador. Por eso ConfirmPaymentIntentRequest no acepta
        payment_method_id, y no hay forma de confirmar un intent contra una tarjeta ajena.

        El navegador NO tiene que capturar el token con el que la pasarela devuelve al titular:
        la inscripcion se cierra con el token que se guardo al abrirla. Solo se admite la
        inscripcion que ESTE intent abrio; cualquier otra responde 404.

        Solo se puede cerrar mientras el intent siga vivo y su cobro no haya empezado; en otro
        caso responde 409 invalid_state_transition. Repetir el cierre cuando la tarjeta ya esta
        asociada devuelve el mismo intent, porque el retorno de la pasarela aterriza en el
        navegador y una recarga vuelve a dispararlo.
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del payment intent (prefijo pi_)
          schema:
            type: string
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        - name: inscriptionId
          in: path
          required: true
          description: Identificador publico de la inscripcion abierta para este intent (prefijo ins_)
          schema:
            type: string
          example: ins_Kw3pR8sYt5Nc2Hj7QmZxVb
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: X-Client-Secret
          in: header
          required: false
          description: >
            client_secret del payment intent. Obligatorio cuando se autentica con clave
            publicable pk_ (sesion de checkout del widget).
          schema:
            type: string
      responses:
        '200':
          description: Payment intent con la tarjeta ya asociada, listo para confirmar el cobro
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntent'
              examples:
                conTarjetaAsociada:
                  summary: Tarjeta guardada y pasarela fijada; el cobro es el confirm de siempre
                  value:
                    id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    object: payment_intent
                    amount_minor: 1499000
                    currency: CLP
                    status: CREATED
                    gateway_id: oneclick
                    payment_method: null
                    redirect_url: null
                    metadata:
                      order_id: "4831"
                    livemode: true
                    created_at: "2026-08-13T10:15:00Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/GatewayUnavailable'

  /v1/refunds:
    post:
      tags: [refunds]
      operationId: createRefund
      summary: Crear un reembolso
      description: >
        Reembolso total (sin amount_minor) o parcial (con amount_minor) sobre un
        payment intent SUCCEEDED. Requiere sk_ e Idempotency-Key.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRefundRequest'
            examples:
              partialRefund:
                summary: Reembolso parcial
                value:
                  payment_intent_id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                  amount_minor: 500000
                  reason: requested_by_customer
                  metadata:
                    ticket: "SOP-1102"
              fullRefund:
                summary: Reembolso total (sin amount_minor)
                value:
                  payment_intent_id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                  reason: duplicate
      responses:
        '201':
          description: Reembolso creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Refund'
              examples:
                pending:
                  summary: Reembolso aceptado y en curso
                  value:
                    id: re_5b1d9c3a7e2f4a08b1c6d3f9
                    object: refund
                    payment_intent_id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    amount_minor: 500000
                    currency: CLP
                    status: PENDING
                    reason: requested_by_customer
                    gateway_reference: null
                    metadata:
                      ticket: "SOP-1102"
                    livemode: true
                    created_at: "2026-08-06T16:05:12Z"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          description: Regla de negocio violada
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://api.apipay.io/problems/amount-exceeds-refundable
                title: Unprocessable Entity
                status: 422
                detail: "El monto solicitado supera el saldo reembolsable del payment intent."
                instance: /v1/refunds
                code: amount_exceeds_refundable
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/GatewayUnavailable'

  /v1/refunds/{id}:
    get:
      tags: [refunds]
      operationId: getRefund
      summary: Obtener un reembolso
      description: >
        Devuelve el reembolso identificado por su id publico (prefijo re_).
        Requiere clave secreta (sk_); el recurso solo es visible para el tenant
        propietario (RLS: nunca se revela la existencia cross-tenant).
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del reembolso (prefijo re_)
          schema:
            type: string
          example: re_5b1d9c3a7e2f4a08b1c6d3f9
      responses:
        '200':
          description: Reembolso
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Refund'
              examples:
                succeeded:
                  summary: Reembolso liquidado por la pasarela
                  value:
                    id: re_5b1d9c3a7e2f4a08b1c6d3f9
                    object: refund
                    payment_intent_id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                    amount_minor: 500000
                    currency: CLP
                    status: SUCCEEDED
                    reason: requested_by_customer
                    gateway_reference: "rev-4831-20260806"
                    metadata:
                      ticket: "SOP-1102"
                    livemode: true
                    created_at: "2026-08-06T16:05:12Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/transactions:
    get:
      tags: [transactions]
      operationId: listTransactions
      summary: Listar transacciones
      description: Listado paginado por cursor, orden created_at DESC. Filtros combinables.
      parameters:
        - name: status
          in: query
          required: false
          description: Filtra por estado de la transaccion
          schema:
            type: string
            enum: [PENDING, APPROVED, REJECTED, REVERSED]
        - name: type
          in: query
          required: false
          description: Filtra por tipo de movimiento
          schema:
            type: string
            enum: [PAYMENT, REFUND, CHARGEBACK]
        - name: from
          in: query
          required: false
          description: Cota inferior de created_at, inclusive, ISO 8601 UTC
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: Cota superior de created_at, exclusiva, ISO 8601 UTC
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Pagina de transacciones
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransactionList'
              examples:
                page:
                  summary: Primera pagina con mas resultados disponibles
                  value:
                    object: list
                    data:
                      - id: txn_7c0a2e9b5d4f4c21e8a3b7d5
                        object: transaction
                        type: PAYMENT
                        status: APPROVED
                        amount_minor: 1499000
                        currency: CLP
                        payment_intent_id: pi_9f2c4a7d1b3e4f60a8c5d2e7
                        refund_id: null
                        gateway_id: webpay
                        gateway_reference: "ord-4831-20260806"
                        created_at: "2026-08-06T14:32:10Z"
                    next_cursor: "eyJjIjoiMjAyNi0wOC0wNlQxNDozMjoxMFoiLCJpZCI6InR4bl83YzBhMmU5YjVkNGY0YzIxIn0"
                    has_more: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/payment-methods/inscriptions:
    post:
      tags: [payment-methods]
      operationId: openPaymentMethodInscription
      summary: Abrir la inscripcion de un medio de pago
      description: >
        Inicia el guardado de un medio de pago. Requiere clave secreta (sk_) y header
        Idempotency-Key: sin el, un reintento abriria otra inscripcion en la pasarela y
        dejaria la anterior huerfana. Aqui NO viaja ningun dato de tarjeta: el titular la
        teclea en el formulario de la pasarela.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OpenInscriptionRequest'
            examples:
              oneclick:
                summary: Inscripcion en Transbank Oneclick
                value:
                  gateway_id: oneclick
                  customer_ref: "cus_42"
                  email: "cliente@example.com"
                  return_url: "https://checkout.apipay.io/inscriptions/return"
      responses:
        '201':
          description: Inscripcion abierta; la respuesta indica como llevar al titular a la pasarela
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethodInscription'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/payment-methods/inscriptions/{id}/confirm:
    post:
      tags: [payment-methods]
      operationId: confirmPaymentMethodInscription
      summary: Cerrar la inscripcion y guardar el medio de pago
      description: >
        Cierra la inscripcion contra la pasarela y crea el medio de pago. Responde con el
        medio, que es el recurso que el comercio usara despues para cobrar.
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico de la inscripcion (prefijo ins_)
          schema:
            type: string
          example: ins_Kw3pR8sYt5Nc2Hj7QmZxVb
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: Medio de pago guardado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethod'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/payment-methods:
    get:
      tags: [payment-methods]
      operationId: listPaymentMethods
      summary: Listar medios de pago guardados
      description: >
        Medios VIGENTES del comercio, paginados por cursor. Los revocados no se listan: se
        conservan en la base porque los payment intents los referencian, pero publicarlos
        invitaria a intentar cobrar con ellos.
      parameters:
        - name: customer_ref
          in: query
          required: false
          description: Filtra por el identificador del cliente en el sistema del comercio
          schema:
            type: string
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Pagina de medios de pago
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethodList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/payment-methods/{id}:
    get:
      tags: [payment-methods]
      operationId: getPaymentMethod
      summary: Obtener un medio de pago guardado
      description: >
        Devuelve el medio de pago por su id, REVOCADOS INCLUIDOS: a diferencia del listado, esta
        lectura no filtra por vigencia, porque los payment intents historicos referencian medios ya
        dados de baja y el comercio necesita poder resolverlos. El estado se lee en el campo
        revoked; intentar cobrar con uno revocado falla en la creacion del intent, no aqui.
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del medio de pago (prefijo pm_)
          schema:
            type: string
          example: pm_Vd7kQ2mXp9Lr4TnB6yWzAe
      responses:
        '200':
          description: Medio de pago
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethod'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [payment-methods]
      operationId: revokePaymentMethod
      summary: Dar de baja un medio de pago guardado
      description: >
        Lo elimina en la pasarela y lo marca revocado. La fila se CONSERVA: los payment
        intents la referencian y borrarla perderia el rastro de con que se cobro. Es
        idempotente y devuelve el recurso ya revocado, no un 204 vacio.
      parameters:
        - name: id
          in: path
          required: true
          description: Identificador publico del medio de pago (prefijo pm_)
          schema:
            type: string
          example: pm_Vd7kQ2mXp9Lr4TnB6yWzAe
      responses:
        '200':
          description: Medio de pago revocado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentMethod'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/gateways/{gatewayId}/webhooks:
    post:
      tags: [gateway-webhooks]
      operationId: receiveGatewayWebhook
      summary: Recibir webhook entrante de una pasarela
      description: >
        Endpoint publico sin API key. La autenticidad se valida con la firma
        especifica de cada pasarela (PaymentGateway.parseAndVerifyWebhook del SPI).
        El evento verificado se persiste en inbound_webhook_events y el intent
        transiciona en una transaccion corta sincrona antes de responder 200;
        los efectos derivados se procesan de forma asincrona via outbox.
      security: []
      parameters:
        - name: gatewayId
          in: path
          required: true
          description: Identificador de la pasarela en gateway_catalog
          schema:
            type: string
            enum: [webpay, mercadopago]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: Payload nativo de la pasarela; se verifica sobre los bytes crudos
          application/x-www-form-urlencoded:
            schema:
              type: object
              additionalProperties: true
              description: >
                Retorno form-encoded de Webpay Plus (token_ws); se verifica confirmando
                la transaccion contra Transbank antes de aceptar el evento.
      responses:
        '200':
          description: Evento recibido; el intent ya transiciono en una transaccion corta
          content:
            application/json:
              schema:
                type: object
                properties:
                  received:
                    type: boolean
                    description: Siempre true cuando el evento fue aceptado
              example:
                received: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: Firma invalida o timestamp fuera de tolerancia
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://api.apipay.io/problems/invalid-webhook-signature
                title: Unauthorized
                status: 401
                detail: "La firma del webhook no pudo ser verificada."
                instance: /v1/gateways/webpay/webhooks
                code: invalid_webhook_signature
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >
        Clave de API del tenant (pk_live_/sk_live_/pk_test_/sk_test_).
        Las sk_ son solo server-to-server; las pk_ solo permiten leer o confirmar
        el payment intent propio presentando su client_secret en el header
        X-Client-Secret.
    OAuth2B2C:
      type: oauth2
      description: >
        OIDC con Azure AD B2C para admin-api (admin.apipay.io, prefijo /admin/v1).
        Roles: PLATFORM_ADMIN, TENANT_ADMIN, TENANT_ANALYST.
        Se declara aqui para que ambos contratos compartan el mismo esquema de
        identidad; ningun endpoint de /v1 lo usa.
      flows:
        authorizationCode:
          authorizationUrl: https://apipayb2c.b2clogin.com/apipayb2c.onmicrosoft.com/B2C_1_signin/oauth2/v2.0/authorize
          tokenUrl: https://apipayb2c.b2clogin.com/apipayb2c.onmicrosoft.com/B2C_1_signin/oauth2/v2.0/token
          scopes:
            https://apipayb2c.onmicrosoft.com/admin-api/api.access: Acceso a admin-api

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: >
        Clave de idempotencia generada por el cliente (se recomienda UUID v4).
        Persistida 24 h; repetirla con el mismo cuerpo devuelve la respuesta original
        con el header Idempotency-Replayed true.
      schema:
        type: string
        maxLength: 255
    Cursor:
      name: cursor
      in: query
      required: false
      description: Cursor opaco devuelto en next_cursor de la pagina anterior
      schema:
        type: string
    Limit:
      name: limit
      in: query
      required: false
      description: Cantidad maxima de elementos por pagina
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

  responses:
    BadRequest:
      description: Solicitud malformada o parametros invalidos
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/validation-error
            title: Bad Request
            status: 400
            detail: "El campo currency debe ser un codigo ISO 4217 de 3 letras."
            instance: /v1/payment-intents
            code: validation_error
    Unauthorized:
      description: API key ausente, invalida o revocada
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/invalid-api-key
            title: Unauthorized
            status: 401
            detail: "La API key entregada en X-API-Key no es valida."
            instance: /v1/payment-intents
            code: invalid_api_key
    PaymentRequired:
      description: La pasarela rechazo la operacion
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/card-declined
            title: Payment Required
            status: 402
            detail: "La pasarela rechazo el pago. Codigo del emisor: insufficient_funds."
            instance: /v1/payment-intents/pi_9f2c4a7d1b3e4f60a8c5d2e7/confirm
            code: card_declined
    Forbidden:
      description: La credencial es valida pero no tiene permisos para la operacion
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/insufficient-permissions
            title: Forbidden
            status: 403
            detail: "Una clave publicable pk_ no puede ejecutar esta operacion."
            instance: /v1/payment-intents/pi_9f2c4a7d1b3e4f60a8c5d2e7/cancel
            code: insufficient_permissions
    NotFound:
      description: Recurso inexistente para este tenant
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/resource-not-found
            title: Not Found
            status: 404
            detail: "No existe un payment intent con ese id para este tenant."
            instance: /v1/payment-intents/pi_000000000000
            code: resource_not_found
    Conflict:
      description: Conflicto de idempotencia o transicion de estado invalida
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/idempotency-key-reuse
            title: Conflict
            status: 409
            detail: "La Idempotency-Key ya fue usada con un cuerpo distinto."
            instance: /v1/payment-intents
            code: idempotency_key_reuse
    UnprocessableEntity:
      description: Regla de negocio violada
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/currency-not-supported
            title: Unprocessable Entity
            status: 422
            detail: "La pasarela webpay no soporta la divisa USD para este tenant."
            instance: /v1/payment-intents
            code: currency_not_supported
    TooManyRequests:
      description: Limite de tasa excedido
      headers:
        Retry-After:
          description: Segundos a esperar antes de reintentar
          schema:
            type: integer
        RateLimit-Limit:
          description: Limite sostenido de la ventana actual
          schema:
            type: integer
        RateLimit-Remaining:
          description: Solicitudes restantes en la ventana actual
          schema:
            type: integer
        RateLimit-Reset:
          description: Segundos hasta el reinicio de la ventana
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/rate-limited
            title: Too Many Requests
            status: 429
            detail: "Se excedio el limite de solicitudes para esta API key."
            instance: /v1/payment-intents
            code: rate_limited
    InternalError:
      description: Falla no controlada; detail nunca expone stack traces ni datos internos
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/internal-error
            title: Internal Server Error
            status: 500
            detail: "Ocurrio un error interno. Reintente o contacte a soporte con el X-Request-Id."
            instance: /v1/payment-intents
            code: internal_error
    GatewayUnavailable:
      description: >
        Circuito abierto o bulkhead lleno hacia la pasarela. La respuesta incluye
        Retry-After y los miembros de extension gateway_id y retry_after_seconds.
      headers:
        Retry-After:
          description: Segundos sugeridos antes de reintentar la operacion
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.apipay.io/problems/gateway-unavailable
            title: Service Unavailable
            status: 503
            detail: "La pasarela webpay no esta disponible en este momento."
            instance: /v1/payment-intents
            code: gateway_unavailable
            gateway_id: webpay
            retry_after_seconds: 30

  schemas:
    PaymentIntent:
      type: object
      required: [id, object, amount_minor, currency, status, gateway_id, livemode, created_at]
      properties:
        id:
          type: string
          description: Identificador publico con prefijo pi_
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        object:
          type: string
          enum: [payment_intent]
          description: Discriminador de tipo de recurso
        amount_minor:
          type: integer
          format: int64
          minimum: 1
          description: Monto en unidades menores de la divisa
          example: 1499000
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: Codigo ISO 4217
          example: CLP
        status:
          type: string
          enum: [CREATED, REQUIRES_ACTION, PROCESSING, SUCCEEDED, FAILED, EXPIRED, CANCELED]
          description: Estado canonico del intent
        client_secret:
          type: string
          nullable: true
          description: >
            Secreto que habilita al checkout-widget a leer y confirmar este intent
            con una clave pk_ (viaja en el header X-Client-Secret). Solo se
            devuelve a claves sk_ y nunca viaja en webhooks.
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7_secret_Vt8yQ1
        gateway_id:
          type: string
          description: Pasarela asignada, segun gateway_catalog
          example: webpay
        payment_method:
          # El campo SI viaja como null, no se omite: PaymentIntentResponse NO marca payment_method
          # con @JsonInclude(NON_NULL) y el ObjectMapper de payment-api serializa con inclusion
          # ALWAYS, asi que el cuerpo lleva "payment_method": null mientras la pasarela no reporta el
          # medio. Lo confirman los ejemplos de 201/200 y el pacto del checkout-widget, que tipa el
          # campo como PaymentMethodSummary | null. Por tanto la nulabilidad hay que CONSERVARLA.
          #
          # Se expresa dentro de PaymentMethodSummary y este campo lo referencia en seco, sin el
          # envoltorio `allOf` + `description` que tenia antes. Dos razones, y ninguna es cosmetica:
          #   1. En OpenAPI 3.0 `nullable` es un modificador de `type`. Aqui no habia `type` hermano
          #      (la forma la aporta el $ref), asi que el `nullable: true` que vivia en esta
          #      propiedad era invalido; de ahi los seis errores oas3-valid-media-example, uno por
          #      cada ejemplo que atraviesa este schema.
          #   2. Marcar nullable solo en la propiedad padre tampoco vale: el $ref de dentro del
          #      allOf sigue exigiendo `type: object` y rechaza el null. Y bajar el nullable al hijo
          #      del allOf, dejandolo aqui envuelto, hace que oasdiff lea un nodo nuevo que "became
          #      nullable" y marque breaking change en el paso siguiente del mismo step. Con el $ref
          #      en seco el nodo payment_method es nullable antes y despues: no hay cambio que
          #      romper, que es exactamente la verdad del contrato.
          # PaymentMethodSummary se referencia UNICAMENTE aqui, asi que llevar alli el nullable no
          # relaja ningun otro consumidor.
          $ref: '#/components/schemas/PaymentMethodSummary'
        redirect_url:
          type: string
          format: uri
          nullable: true
          description: URL de la pasarela a la que redirigir al pagador cuando status es REQUIRES_ACTION
        metadata:
          type: object
          additionalProperties:
            type: string
          maxProperties: 50
          description: Pares clave-valor definidos por el comercio
        livemode:
          type: boolean
          description: false cuando el intent fue creado con una clave *_test_
        created_at:
          type: string
          format: date-time
          description: Instante de creacion en UTC
          example: "2026-08-06T14:30:00Z"
        checkout:
          # Sin `nullable`, y no por comodidad: PaymentIntentResponse marca checkout con
          # @JsonInclude(NON_NULL), de modo que cuando no aplica la clave se OMITE del cuerpo en vez
          # de emitirse como null. La ausencia ya la expresa el no estar en `required`; declararlo
          # nullable documentaria un valor que el servicio nunca produce (y ademas era invalido en
          # 3.0, al no haber `type` hermano junto al $ref).
          description: >
            Configuracion de checkout para el widget. Solo presente cuando se
            autentica con pk_ + header X-Client-Secret; omitido con sk_.
          allOf:
            - $ref: '#/components/schemas/CheckoutConfig'
        # Los dos campos de abajo los serializa PaymentIntentResponse desde siempre, pero no
        # estaban declarados aqui: eran superficie publica indocumentada, invisible para los cuatro
        # SDKs. Sin checkout_url un comercio no puede mandar al comprador al checkout alojado, que
        # es justo el flujo de "boton de pago" del hub.
        #
        # Sin `nullable`, por el mismo motivo que `checkout`: los dos llevan @JsonInclude(NON_NULL),
        # asi que cuando no aplican la clave se OMITE en vez de emitirse como null. Que puedan
        # faltar ya lo dice el no estar en `required`.
        merchant_return_url:
          type: string
          format: uri
          description: >
            URL de la tienda a la que la pagina de resultado del checkout ofrece volver
            (boton "Volver a la tienda"). Es la que el comercio declaro al crear el intent.
            Se omite si no la declaro.
          example: https://tienda-andina.cl/pedidos/4831
        checkout_url:
          type: string
          format: uri
          description: >
            URL del checkout ALOJADO a la que el comercio manda al comprador: la pagina de
            ApiPay donde elige medio de pago y paga. Es la pieza del flujo tipo "boton de
            pago": se crea el intent SIN gateway_id y se redirige aqui, y el comprador ve
            los medios que el comercio tiene habilitados.

            SOLO SE EMITE AL CREAR EL INTENT (201). Es el unico momento en que existe el
            client_secret en claro, y la URL lo lleva en el fragmento; una lectura posterior
            con GET no puede reconstruirla y omite el campo. Si se pierde, hay que crear
            otro intent.

            Se omite tambien si el despliegue no tiene configurada la base del checkout o si
            el tenant no declaro su clave publicable: sin ella la pagina no puede
            autenticarse contra la API, y una URL a medias es peor que ninguna.
          example: https://checkout.apipay.io/session?pk=pk_test_51H#client_secret=pi_9f2c4a7d1b3e4f60_secret_Vt8yQ1

    PaymentMethodSummary:
      type: object
      # `nullable` vive aqui, junto a su `type`: es la unica forma valida en OpenAPI 3.0 de declarar
      # nullable un campo cuya forma llega por $ref (ver el comentario de PaymentIntent.payment_method,
      # su unico punto de uso).
      nullable: true
      description: >
        Metadatos no sensibles del medio de pago expuestos por la pasarela, o null mientras la
        pasarela todavia no ha reportado con que se cobro.
        Nunca contiene PAN, CVV ni track data (alcance PCI DSS SAQ A).
      properties:
        type:
          type: string
          description: Familia del medio de pago (card, wallet, bank_transfer)
          example: card
        brand:
          type: string
          nullable: true
          description: Marca reportada por la pasarela
          example: visa
        last4:
          type: string
          nullable: true
          pattern: '^[0-9]{4}$'
          description: Ultimos cuatro digitos, unico fragmento del numero que existe en el sistema
          example: "4242"
        exp_month:
          type: integer
          nullable: true
          minimum: 1
          maximum: 12
          description: Mes de expiracion
        exp_year:
          type: integer
          nullable: true
          description: Anio de expiracion con cuatro digitos

    CheckoutConfig:
      type: object
      description: >
        Configuracion que el checkout-widget necesita para renderizar la sesion.
        Solo se expone al autenticar con pk_ + header X-Client-Secret.
      properties:
        tenant_name:
          type: string
          description: Nombre comercial mostrado en el widget
          example: Tienda Andina
        logo_url:
          type: string
          format: uri
          nullable: true
          description: Logotipo del comercio
        allowed_origins:
          type: array
          description: Origenes autorizados para embeber el widget
          items:
            type: string
          example: ["https://tienda-andina.cl"]
        payment_methods:
          type: array
          description: Pasarelas habilitadas para esta sesion, en orden de preferencia
          items:
            type: object
            properties:
              gateway_id:
                type: string
                description: Codigo de la pasarela en gateway_catalog
                example: webpay
              public_config:
                type: object
                additionalProperties: true
                description: Configuracion publica no sensible de la pasarela (ej. publishable key)
        theme:
          type: object
          additionalProperties: true
          description: Personalizacion visual del widget definida por el tenant

    OpenInscriptionRequest:
      type: object
      required: [gateway_id, customer_ref, email, return_url]
      description: >
        Apertura de una inscripcion. Ningun campo de tarjeta: el PAN se teclea en el
        formulario de la pasarela y jamas entra a ApiPay (PCI DSS SAQ A).
      properties:
        gateway_id:
          type: string
          description: Pasarela que guardara el medio de pago
          example: oneclick
        customer_ref:
          type: string
          maxLength: 128
          description: >
            Identificador del cliente EN EL SISTEMA DEL COMERCIO. No viaja a la pasarela: alla
            se envia un identificador opaco que genera ApiPay, unico por medio de pago, porque
            reutilizarlo por cliente haria que la pasarela invalidara la tarjeta anterior en
            cada inscripcion nueva.
          example: cus_42
        email:
          type: string
          format: email
          maxLength: 255
          description: Correo del titular; la pasarela lo usa en su formulario
        return_url:
          type: string
          format: uri
          description: URL a la que la pasarela devuelve el navegador con el token de inscripcion

    OpenPaymentIntentInscriptionRequest:
      type: object
      description: >
        Apertura de una inscripcion PARA UN PAYMENT INTENT, desde el checkout alojado. Los tres
        campos son opcionales y el cuerpo entero puede faltar: todo se deriva del intent cuando
        no viene.

        No existe customer_ref: lo deriva el servidor del intent (metadata.customer_ref si el
        comercio lo puso, y si no el id publico del intent), para que una pagina no pueda
        inscribir una tarjeta a nombre de otro cliente del comercio. Ningun campo de tarjeta:
        el PAN se teclea en el formulario de la pasarela (PCI DSS SAQ A).
      properties:
        gateway_id:
          type: string
          maxLength: 64
          description: >
            Pasarela que guardara la tarjeta, tal como la eligio el comprador en el selector.
            Si se omite se usa la que el intent ya tenga fijada; si pide una distinta de esa,
            responde 400 validation_error, porque la tarjeta quedaria inscrita en una pasarela
            que no es la que va a cobrar.
          example: oneclick
        email:
          type: string
          format: email
          maxLength: 320
          description: >
            Correo del titular; la pasarela lo pide en su formulario. Si se omite se busca en
            metadata.customer_email del intent. El customer_email de la creacion no sirve: no
            se persiste en ninguna parte.
          example: "cliente@example.com"
        return_url:
          type: string
          format: uri
          description: >
            URL a la que la pasarela devuelve el navegador con el token de inscripcion. Si se
            omite se reutiliza la return_url del intent: la inscripcion vuelve por el mismo
            camino que el pago.
          example: "https://checkout.apipay.io/session"

    PaymentMethodInscription:
      type: object
      required: [id, object, status, gateway_id, customer_ref, expires_at, created_at]
      properties:
        id:
          type: string
          description: Identificador publico con prefijo ins_
          example: ins_Kw3pR8sYt5Nc2Hj7QmZxVb
        object:
          type: string
          enum: [payment_method_inscription]
        status:
          type: string
          enum: [REQUIRES_ACTION, COMPLETED, FAILED, EXPIRED]
        gateway_id:
          type: string
          example: oneclick
        customer_ref:
          type: string
          example: cus_42
        last_error:
          type: string
          description: Motivo del fallo, si lo hubo
        expires_at:
          type: string
          format: date-time
          description: Instante a partir del cual el token de la pasarela ya no sirve
        created_at:
          type: string
          format: date-time
        redirect:
          $ref: '#/components/schemas/InscriptionRedirect'

    InscriptionRedirect:
      type: object
      required: [method, url, field_name, token]
      description: >
        Como llevar al titular al formulario de la pasarela. Lleva method y field_name, y no
        solo una URL, porque el formulario de Transbank NO se abre con un GET: hay que hacer un
        POST con el token en un campo. Solo aparece cuando la inscripcion espera accion.
      properties:
        method:
          type: string
          enum: [POST]
        url:
          type: string
          format: uri
        field_name:
          type: string
          description: Nombre del campo que transporta el token
          example: TBK_TOKEN
        token:
          type: string
          description: Valor del campo

    PaymentMethod:
      type: object
      required: [id, object, gateway_id, customer_ref, type, revoked, created_at]
      description: >
        Medio de pago guardado. No hay ningun campo con el que reconstruir una tarjeta, y el
        token de la pasarela NO se publica: el comercio cobra referenciando el id.
      properties:
        id:
          type: string
          description: Identificador publico con prefijo pm_
          example: pm_Vd7kQ2mXp9Lr4TnB6yWzAe
        object:
          type: string
          enum: [payment_method]
        gateway_id:
          type: string
          example: oneclick
        customer_ref:
          type: string
          example: cus_42
        type:
          type: string
          enum: [card, wallet, bank_transfer]
        brand:
          type: string
          example: Visa
        last4:
          type: string
          pattern: '^[0-9]{4}$'
          example: "6623"
        exp_month:
          type: integer
          minimum: 1
          maximum: 12
        exp_year:
          type: integer
        revoked:
          type: boolean
        revoked_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    PaymentMethodList:
      type: object
      required: [object, data, has_more]
      properties:
        object:
          type: string
          enum: [list]
        data:
          type: array
          items:
            $ref: '#/components/schemas/PaymentMethod'
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean

    CreatePaymentIntentRequest:
      type: object
      required: [amount_minor, currency]
      properties:
        amount_minor:
          type: integer
          format: int64
          minimum: 1
          description: Monto a cobrar en unidades menores de la divisa
          example: 1499000
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: Codigo ISO 4217
          example: CLP
        gateway_id:
          type: string
          description: >
            Pasarela a usar. OMITIRLA es el modo hub: el intent nace sin pasarela, el
            checkout ofrece al comprador los medios habilitados del comercio, y la que
            elija se fija en el confirm (campo gateway_id de ConfirmPaymentIntentRequest).
            Indicarla aqui salta el selector y fija la pasarela desde el principio, que
            es lo que necesita un cobro server-to-server. Si se omite y el confirm
            tampoco la indica, se aplica la de mayor prioridad del tenant.
          example: webpay
        description:
          type: string
          maxLength: 500
          description: Descripcion del cobro, visible en el backoffice
        customer_email:
          type: string
          format: email
          description: Correo del pagador, usado por algunas pasarelas para el comprobante
        return_url:
          type: string
          format: uri
          description: URL del comercio a la que vuelve el pagador tras un flujo de redireccion
        merchant_return_url:
          type: string
          format: uri
          description: >
            URL de la tienda a la que volver desde la pagina de resultado del checkout
            (boton "Volver a la tienda"). No participa del flujo de pago: si se omite,
            la pagina de resultado no ofrece la vuelta.
        payment_method_id:
          type: string
          description: >
            Medio de pago GUARDADO con el que cobrar (pm_...), previamente inscrito con
            POST /v1/payment-methods. Es un cobro sin el titular presente: no hay
            selector ni redireccion, el confirm autoriza contra la credencial guardada.
            La pasarela se DERIVA del medio —una tarjeta inscrita en Oneclick solo se
            cobra en Oneclick—, asi que mandar ademas un gateway_id distinto responde
            400 validation_error. Un medio revocado o inexistente, tambien.
          example: pm_9f8e7d6c5b4a3210
        metadata:
          type: object
          additionalProperties:
            type: string
          maxProperties: 50
          description: >
            Pares clave-valor definidos por el comercio. En un cobro con medio guardado
            de Oneclick, la clave reservada "installments" fija el numero de cuotas
            (entero no negativo); si se omite, se cobra sin cuotas.

    ConfirmPaymentIntentRequest:
      type: object
      description: >
        En flujos de redireccion puede ir vacio; en flujos con hosted fields lleva
        el token del medio de pago emitido por la pasarela (nunca datos de tarjeta).
      properties:
        gateway_id:
          type: string
          description: >
            Pasarela elegida por el comprador en el selector del checkout. Solo tiene
            efecto si el intent se creo SIN gateway_id: ese es el modo hub, donde el
            comercio pide el cobro y el comprador decide con que pagarlo. Una vez que
            el cobro esta en curso (hay referencia de la pasarela) mandar otra distinta
            responde 422: un token de una pasarela no significa nada en otra.
          example: webpay
        payment_method_token:
          type: string
          description: Token generado por los hosted fields o Bricks de la pasarela
        return_url:
          type: string
          format: uri
          description: URL de retorno, si no se fijo al crear el intent
        gateway_params:
          type: object
          additionalProperties:
            type: string
          description: >
            Parametros no sensibles del retorno de la pasarela. El caso principal es
            token_ws de Webpay Plus, que dispara la fase de commit. El servidor tambien
            los toma de la query string cuando el navegador vuelve al comercio.
          example:
            token_ws: "01ab3e2061bb7edb14e124155d3ba7d4"

    CancelPaymentIntentRequest:
      type: object
      description: Motivo opcional de la cancelacion, registrado en transaction_events
      properties:
        reason:
          type: string
          enum: [requested_by_customer, duplicate, fraudulent, abandoned]
          description: Motivo de la cancelacion

    Refund:
      type: object
      required: [id, object, payment_intent_id, amount_minor, currency, status, livemode, created_at]
      properties:
        id:
          type: string
          description: Identificador publico con prefijo re_
          example: re_5b1d9c3a7e2f4a08b1c6d3f9
        object:
          type: string
          enum: [refund]
          description: Discriminador de tipo de recurso
        payment_intent_id:
          type: string
          description: Payment intent reembolsado
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        amount_minor:
          type: integer
          format: int64
          minimum: 1
          description: Monto reembolsado en unidades menores
          example: 500000
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: Codigo ISO 4217, siempre igual al del payment intent
          example: CLP
        status:
          type: string
          enum: [PENDING, SUCCEEDED, FAILED]
          description: Estado canonico del reembolso
        reason:
          type: string
          nullable: true
          maxLength: 500
          description: Motivo declarado por el comercio
        gateway_reference:
          type: string
          nullable: true
          description: Referencia del reembolso en la pasarela
        metadata:
          type: object
          additionalProperties:
            type: string
          maxProperties: 50
          description: Pares clave-valor definidos por el comercio
        livemode:
          type: boolean
          description: false cuando el reembolso pertenece a datos de prueba
        created_at:
          type: string
          format: date-time
          description: Instante de creacion en UTC
          example: "2026-08-06T16:05:12Z"

    CreateRefundRequest:
      type: object
      required: [payment_intent_id]
      properties:
        payment_intent_id:
          type: string
          description: Payment intent SUCCEEDED a reembolsar
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        amount_minor:
          type: integer
          format: int64
          minimum: 1
          description: Si se omite, se reembolsa el saldo total reembolsable
        reason:
          type: string
          enum: [requested_by_customer, duplicate, fraudulent]
          description: Motivo del reembolso
        metadata:
          type: object
          additionalProperties:
            type: string
          maxProperties: 50
          description: Pares clave-valor definidos por el comercio

    Transaction:
      type: object
      required: [id, object, type, status, amount_minor, currency, gateway_id, created_at]
      properties:
        id:
          type: string
          description: Identificador publico con prefijo txn_
          example: txn_7c0a2e9b5d4f4c21e8a3b7d5
        object:
          type: string
          enum: [transaction]
          description: Discriminador de tipo de recurso
        type:
          type: string
          enum: [PAYMENT, REFUND, CHARGEBACK]
          description: Naturaleza del movimiento
        status:
          type: string
          enum: [PENDING, APPROVED, REJECTED, REVERSED]
          description: Estado canonico de la transaccion
        amount_minor:
          type: integer
          format: int64
          description: Monto del movimiento en unidades menores
          example: 1499000
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: Codigo ISO 4217
          example: CLP
        payment_intent_id:
          type: string
          nullable: true
          description: Payment intent asociado
          example: pi_9f2c4a7d1b3e4f60a8c5d2e7
        refund_id:
          type: string
          nullable: true
          description: Reembolso asociado cuando type es REFUND
          example: re_5b1d9c3a7e2f4a08b1c6d3f9
        gateway_id:
          type: string
          description: Pasarela que proceso el movimiento
          example: webpay
        gateway_reference:
          type: string
          nullable: true
          description: Identificador de la operacion en la pasarela
        created_at:
          type: string
          format: date-time
          description: Instante del movimiento en UTC
          example: "2026-08-06T14:32:10Z"

    TransactionList:
      type: object
      required: [object, data, has_more]
      description: Envelope de listado paginado por cursor
      properties:
        object:
          type: string
          enum: [list]
          description: Discriminador del envelope de lista
        data:
          type: array
          description: Elementos de la pagina, ordenados por created_at DESC
          items:
            $ref: '#/components/schemas/Transaction'
        next_cursor:
          type: string
          nullable: true
          description: Cursor opaco para la pagina siguiente; null si no hay mas resultados
        has_more:
          type: boolean
          description: true cuando existe al menos una pagina adicional

    Problem:
      type: object
      description: >
        Error segun RFC 9457. Ademas de los miembros estandar y code, admite
        miembros de extension especificos del problema (p. ej. gateway_id y
        retry_after_seconds en el 503 gateway_unavailable).
      required: [type, title, status]
      additionalProperties: true
      properties:
        type:
          type: string
          format: uri
          description: URI del problema, patron https://api.apipay.io/problems/{code-en-kebab-case}
          example: https://api.apipay.io/problems/validation-error
        title:
          type: string
          description: Titulo corto y estable del tipo de problema
          example: Bad Request
        status:
          type: integer
          description: Codigo de estado HTTP repetido en el cuerpo
          example: 400
        detail:
          type: string
          description: Explicacion legible por humanos de esta ocurrencia
        instance:
          type: string
          description: Path de la solicitud que produjo el error
          example: /v1/payment-intents
        code:
          type: string
          description: Codigo de error estable legible por maquina (extension ApiPay)
          example: validation_error
