openapi: 3.1.0
info:
  title: Portal Emisión - API de Integraciones
  version: 1.0.0
  summary: Reporte y administración de ventas de asistencia al viajero.
  description: |
    API para integraciones autorizadas. Cada credencial accede únicamente al espacio
    que tiene asignado en Portal Emisión; este contexto nunca se recibe en las órdenes.

    En una venta reportada, el integrador declara que la venta externa fue procesada y
    pagada correctamente. Portal Emisión recalcula precios y valida plan, edades, días,
    territorios, promociones, recargos, restricciones y riders con su configuración vigente.
  contact:
    name: Equipo de Integraciones
servers:
  - url: https://sewl-qa.terrawindglobalprotection.net/api/integrations/v1
    description: Pruebas (QA)
  - url: https://portalemision.com/api/integrations/v1
    description: Producción
tags:
  - name: Autenticación
  - name: Catálogo
  - name: Ventas reportadas
  - name: Órdenes
  - name: Webhooks
security:
  - bearerAuth: []
paths:
  /auth/token:
    post:
      tags: [Autenticación]
      summary: Obtener token de integración
      description: El token es de corta duración y se reutiliza durante su vigencia. No se genera un token por método.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [email, password]
              properties:
                email: { type: string, format: email, example: integracion@empresa.com }
                password: { type: string, format: password }
      responses:
        '200':
          description: Token emitido
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TokenResponse' }
              examples:
                issued: { $ref: '#/components/examples/TokenIssued' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /catalog/countries:
    get:
      tags: [Catálogo]
      summary: Listar países de origen y nacionalidad
      responses:
        '200':
          description: Países activos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Country' }
  /catalog/territories:
    get:
      tags: [Catálogo]
      summary: Listar territorios de destino
      description: Las órdenes usan territorios comerciales, no países de destino.
      responses:
        '200':
          description: Territorios activos
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Territory' }
  /catalog/plans:
    get:
      tags: [Catálogo]
      summary: Listar planes disponibles en el espacio de integración
      responses:
        '200':
          description: Catálogo comercial disponible para la integración
  /catalog/plans/{planId}:
    get:
      tags: [Catálogo]
      summary: Consultar condiciones, beneficios y riders de un plan
      parameters:
        - $ref: '#/components/parameters/PlanId'
      responses:
        '200': { description: Detalle comercial del plan }
        '404': { $ref: '#/components/responses/NotFound' }
  /reported-orders:
    post:
      tags: [Ventas reportadas]
      summary: Reportar una venta externa procesada y pagada
      description: |
        No recibe cotización, forma de pago, referencia de pago ni importes. El servidor
        calcula todos los montos. Si la solicitud no cumple las reglas, se conserva como
        rechazada para consulta y no genera una orden válida ni activa una cobertura.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportedOrderRequest' }
            examples:
              standard:
                $ref: '#/components/examples/StandardReportedOrder'
      responses:
        '201':
          description: Venta aceptada y orden activa
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportedOrderResponse' }
              examples:
                accepted: { $ref: '#/components/examples/ReportedOrderAccepted' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /reported-orders/brazil:
    post:
      tags: [Ventas reportadas]
      summary: Reportar una venta Brasil y emitirla con HERO
      description: |
        Extiende la solicitud estándar con los datos regulatorios y de dirección exigidos
        por HERO. Acepta exactamente un pasajero. La orden puede quedar `issue_failed` si
        Portal Emisión la aceptó pero HERO no confirmó la emisión; este estado no equivale
        a una solicitud rechazada.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BrazilReportedOrderRequest' }
            examples:
              brazil:
                $ref: '#/components/examples/BrazilReportedOrder'
      responses:
        '201':
          description: Orden aceptada y emisión HERO confirmada
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BrazilReportedOrderResponse' }
              examples:
                issued: { $ref: '#/components/examples/BrazilIssued' }
        '202':
          description: Orden aceptada localmente; emisión HERO pendiente o fallida
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BrazilReportedOrderResponse' }
              examples:
                pending: { $ref: '#/components/examples/BrazilIssuePending' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /orders/{orderId}:
    parameters:
      - $ref: '#/components/parameters/OrderId'
    get:
      tags: [Órdenes]
      summary: Consultar una orden por su identificador público
      responses:
        '200':
          description: Estado actual de la orden
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportedOrderResponse' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Órdenes]
      summary: Corregir contacto de emergencia o contacto de pasajeros
      description: Solo se permite antes del inicio de vigencia, usando America/Caracas.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OrderUpdateRequest' }
      responses:
        '200': { description: Orden actualizada }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Órdenes]
      summary: Cancelar una orden
      description: Solo se permite antes del inicio de vigencia. Brasil requiere confirmación del proveedor.
      responses:
        '200': { description: Orden cancelada }
        '409': { $ref: '#/components/responses/Conflict' }
  /submissions:
    get:
      tags: [Ventas reportadas]
      summary: Consultar intentos aceptados, rechazados o fallidos
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [processing, accepted, rejected, failed] }
        - name: external_code
          in: query
          schema: { type: string }
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        '200': { description: Historial paginado de la integración autenticada }
  /submissions/{submissionId}:
    get:
      tags: [Ventas reportadas]
      summary: Consultar el resultado y errores de un intento
      parameters:
        - name: submissionId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: Detalle del intento; los errores se muestran si fue rechazado }
        '404': { $ref: '#/components/responses/NotFound' }
  /webhooks:
    get:
      tags: [Webhooks]
      summary: Listar receptores configurados por la integración
      responses:
        '200': { description: Receptores registrados; el secreto nunca se devuelve }
    post:
      tags: [Webhooks]
      summary: Registrar un receptor HTTPS
      description: El secreto de firma se entrega una sola vez en esta respuesta.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEndpointRequest' }
      responses:
        '201':
          description: Receptor creado y secreto entregado
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookEndpointResponse' }
              examples:
                registered: { $ref: '#/components/examples/WebhookRegistered' }
  /orders/{orderId}/cancellation-requests:
    post:
      tags: [Órdenes]
      summary: Solicitar a HERO la cancelación de una orden Brasil
      description: La solicitud aprobada debe confirmarse dentro de la ventana informada por HERO.
      parameters:
        - $ref: '#/components/parameters/OrderId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: integer, minimum: 1, maximum: 9 }
      responses:
        '202': { description: Solicitud aprobada; devuelve el límite de confirmación }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { description: HERO no está disponible }
  /orders/{orderId}/cancellation-confirmations:
    post:
      tags: [Órdenes]
      summary: Confirmar la cancelación Brasil dentro de la ventana de HERO
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200': { description: Orden cancelada localmente y en HERO }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { description: HERO no está disponible }
  /webhooks/{endpointId}:
    parameters:
      - name: endpointId
        in: path
        required: true
        schema: { type: integer }
    patch:
      tags: [Webhooks]
      summary: Actualizar URL, eventos o estado del receptor
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEndpointRequest' }
      responses:
        '200': { description: Receptor actualizado }
    delete:
      tags: [Webhooks]
      summary: Eliminar un receptor
      responses:
        '204': { description: Receptor eliminado }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Llave única por intento lógico. Reutilice la misma llave al reintentar por error de red.
      schema: { type: string, minLength: 1, maxLength: 100, pattern: '^[A-Za-z0-9._:-]+$' }
      example: sale-EXT-2026-000184-v1
    PlanId:
      name: planId
      in: path
      required: true
      schema: { type: integer }
    OrderId:
      name: orderId
      in: path
      required: true
      description: UUID público generado por Portal Emisión.
      schema: { type: string, format: uuid }
  schemas:
    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in, scopes]
      properties:
        access_token: { type: string }
        token_type: { type: string, const: Bearer }
        expires_in: { type: integer, example: 900 }
        scopes:
          type: array
          items: { type: string }
    Country:
      type: object
      properties:
        code: { type: string, minLength: 2, maxLength: 2, example: VE }
        name: { type: string, example: Venezuela }
        territory_id: { type: string, example: '3' }
    Territory:
      type: object
      properties:
        id: { type: string, example: '4' }
        code: { type: string, example: EUR }
        name: { type: string, example: Europa }
    Phone:
      type: object
      additionalProperties: false
      required: [country_code, number]
      properties:
        country_code: { type: string, example: '58' }
        number: { type: string, example: '4121234567' }
    EmergencyContact:
      type: object
      additionalProperties: false
      required: [full_name, email, phone]
      properties:
        full_name: { type: string, example: María Pérez }
        email: { type: string, format: email, example: maria@example.com }
        phone: { $ref: '#/components/schemas/Phone' }
    Document:
      type: object
      additionalProperties: false
      required: [type, number]
      properties:
        type: { type: string, example: passport }
        number: { type: string, example: P12345678 }
    Passenger:
      type: object
      additionalProperties: false
      required: [first_name, last_name, birth_date, nationality, document, email, phone, gender]
      properties:
        first_name: { type: string, example: Juan }
        last_name: { type: string, example: Rodríguez }
        birth_date: { type: string, format: date, example: '1988-04-21' }
        nationality: { type: string, minLength: 2, maxLength: 2, example: VE }
        document: { $ref: '#/components/schemas/Document' }
        email: { type: string, format: email, example: juan@example.com }
        phone: { $ref: '#/components/schemas/Phone' }
        gender: { type: string, enum: [M, F] }
        medical_conditions:
          type: [string, 'null']
          description: Opcional; no envíe el campo cuando no aplique.
    Trip:
      type: object
      additionalProperties: false
      required: [origin_country, destination_territory_id, starts_at, ends_at]
      properties:
        origin_country: { type: string, minLength: 2, maxLength: 2, example: VE }
        destination_territory_id: { type: string, example: '4' }
        starts_at: { type: string, format: date, example: '2026-09-10' }
        ends_at: { type: string, format: date, example: '2026-09-20' }
        annual_interval:
          type: boolean
          default: false
          description: Úselo únicamente cuando el plan comercial corresponda a multiviaje anual.
    RiderSelection:
      type: object
      additionalProperties: false
      required: [id, passenger_documents]
      properties:
        id: { type: integer, example: 12 }
        passenger_documents:
          type: array
          minItems: 1
          uniqueItems: true
          items: { type: string }
    ReportedOrderRequest:
      type: object
      additionalProperties: false
      required: [external_code, plan_id, trip, emergency_contact, passengers]
      properties:
        external_code:
          type: string
          maxLength: 100
          description: Código generado por el sistema integrador y único dentro de su espacio asignado.
          example: EXT-2026-000184
        plan_id: { type: integer, example: 108 }
        trip: { $ref: '#/components/schemas/Trip' }
        emergency_contact: { $ref: '#/components/schemas/EmergencyContact' }
        passengers:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: '#/components/schemas/Passenger' }
        riders:
          type: array
          items: { $ref: '#/components/schemas/RiderSelection' }
    BrazilData:
      type: object
      additionalProperties: false
      required: [has_cpf, civil_status_id, address]
      properties:
        has_cpf: { type: boolean }
        civil_status_id: { type: integer, minimum: 1, maximum: 10 }
        responsible:
          type: object
          description: Obligatorio cuando has_cpf es false.
          properties:
            full_name: { type: string }
            cpf: { type: string }
        address:
          type: object
          required: [cep, street, number, neighborhood, city, state]
          properties:
            cep: { type: string, pattern: '^\d{8}$' }
            street: { type: string }
            number: { type: string }
            complement: { type: [string, 'null'] }
            neighborhood: { type: string }
            city: { type: string }
            state: { type: string, pattern: '^[A-Za-z]{2}$' }
    BrazilReportedOrderRequest:
      type: object
      additionalProperties: false
      required: [external_code, plan_id, trip, emergency_contact, passengers, brazil]
      properties:
        external_code: { type: string, maxLength: 100 }
        plan_id: { type: integer }
        trip: { $ref: '#/components/schemas/Trip' }
        emergency_contact: { $ref: '#/components/schemas/EmergencyContact' }
        passengers:
          type: array
          minItems: 1
          maxItems: 1
          items: { $ref: '#/components/schemas/Passenger' }
        riders:
          type: array
          items: { $ref: '#/components/schemas/RiderSelection' }
        brazil: { $ref: '#/components/schemas/BrazilData' }
    OrderUpdateRequest:
      type: object
      properties:
        emergency_contact: { $ref: '#/components/schemas/EmergencyContact' }
        passengers:
          type: array
          items:
            type: object
            required: [document_number]
            properties:
              document_number: { type: string }
              email: { type: string, format: email }
              phone: { $ref: '#/components/schemas/Phone' }
              medical_conditions: { type: [string, 'null'] }
    WebhookEndpointRequest:
      type: object
      properties:
        url: { type: string, format: uri, pattern: '^https://' }
        events:
          type: array
          uniqueItems: true
          items:
            type: string
            enum: [order.created, order.updated, order.cancelled, order.issue_failed]
        is_active: { type: boolean }
    ReportedOrderResponse:
      type: object
      properties:
        order:
          type: object
          properties:
            order_id: { type: string, format: uuid }
            system_code: { type: string, example: TW-7F21D8KQ }
            external_code: { type: string, example: EXT-2026-000184 }
            status: { type: string, enum: [active, issue_failed, cancelled] }
            validity:
              type: object
              properties:
                starts_at: { type: string, format: date }
                ends_at: { type: string, format: date }
                timezone: { type: string, const: America/Caracas }
            plan:
              type: object
              properties:
                id: { type: string, example: '108' }
                name: { type: [string, 'null'], example: Plan Global Plus }
            amounts:
              type: object
              properties:
                currency: { type: string, example: USD }
                base: { type: number }
                riders: { type: number }
                total: { type: number }
                calculated_at: { type: string, format: date-time }
            created_at: { type: string, format: date-time }
        payment_attestation:
          type: object
          properties:
            accepted: { type: boolean, const: true }
            message: { type: string }
        request_id: { type: string, format: uuid }
    BrazilReportedOrderResponse:
      allOf:
        - $ref: '#/components/schemas/ReportedOrderResponse'
        - type: object
          properties:
            provider:
              type: object
              required: [name, status, message]
              properties:
                name: { type: string, const: HERO }
                status: { type: string, example: emitida }
                message: { type: string }
    WebhookEndpointResponse:
      type: object
      required: [endpoint, secret]
      properties:
        endpoint:
          type: object
          required: [id, url, events, is_active]
          properties:
            id: { type: integer, example: 14 }
            url: { type: string, format: uri }
            events:
              type: array
              items: { type: string }
            is_active: { type: boolean }
        secret:
          type: string
          description: Se muestra únicamente al crear el receptor.
          example: whsec_MuestraSoloUnaVez_8P4x
    Error:
      type: object
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: validation_failed }
            message: { type: string }
            details: { type: object, additionalProperties: true }
        request_id: { type: string, format: uuid }
  responses:
    Unauthorized:
      description: Credenciales o token inválidos
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: { code: invalid_credentials, message: Credenciales inválidas. }
    Forbidden:
      description: La cuenta, operación o alcance no está habilitado
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: { code: forbidden, message: La operación no está permitida para esta integración. }
            request_id: 0a5cc4c7-dc6c-45ad-ac7a-65085ef08e46
    NotFound:
      description: Recurso no encontrado o no disponible en el espacio asignado
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: { code: not_found, message: Recurso no encontrado o no disponible en este espacio. }
            request_id: 0a5cc4c7-dc6c-45ad-ac7a-65085ef08e46
    Conflict:
      description: Conflicto de idempotencia, código externo o límite temporal
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            duplicate: { $ref: '#/components/examples/ExternalCodeConflict' }
    ValidationError:
      description: Solicitud rechazada por estructura o reglas comerciales
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            ineligible: { $ref: '#/components/examples/PlanNotEligible' }
    TooManyRequests:
      description: Se excedió el límite temporal de intentos
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: { code: too_many_attempts, message: Demasiados intentos. Intente nuevamente más tarde. }
  examples:
    TokenIssued:
      value:
        access_token: eyJhbGciOiJIUzI1NiJ9...
        token_type: Bearer
        expires_in: 900
        scopes: [catalog:read, orders:report, orders:read, orders:update, orders:cancel, webhooks:manage]
    ReportedOrderAccepted:
      value:
        order:
          order_id: 9f23c1de-8eb4-4e89-a90c-1c99e8f0da31
          system_code: TW-7F21D8KQ
          external_code: EXT-2026-000184
          status: active
          validity: { starts_at: '2026-09-10', ends_at: '2026-09-20', timezone: America/Caracas }
          plan: { id: '108', name: Plan Global Plus }
          amounts: { currency: USD, base: 128.50, riders: 12.00, total: 140.50, calculated_at: '2026-08-05T10:34:21-04:00' }
          created_at: '2026-08-05T10:34:21-04:00'
        payment_attestation:
          accepted: true
          message: La integración declara que la venta externa fue procesada y pagada correctamente.
        request_id: 77e6cf9e-6b7d-4c42-9bb4-937ec0c14d42
    BrazilIssued:
      value:
        order:
          order_id: 9f23c1de-8eb4-4e89-a90c-1c99e8f0da31
          system_code: BR-2AK91MZQ
          external_code: BR-2026-00991
          status: active
          validity: { starts_at: '2026-10-02', ends_at: '2026-10-12', timezone: America/Caracas }
        provider: { name: HERO, status: emitida, message: La emisión fue confirmada por HERO. }
        request_id: be6f70a5-3270-49eb-a495-f802c07ac943
    BrazilIssuePending:
      value:
        order:
          order_id: 9f23c1de-8eb4-4e89-a90c-1c99e8f0da31
          system_code: BR-2AK91MZQ
          external_code: BR-2026-00991
          status: issue_failed
        provider: { name: HERO, status: error, message: 'La venta fue aceptada localmente, pero la emisión con HERO requiere reintento.' }
        request_id: be6f70a5-3270-49eb-a495-f802c07ac943
    ExternalCodeConflict:
      value:
        error:
          code: external_code_already_exists
          message: El código externo ya fue aceptado en este espacio.
          details: { external_code: [Debe ser único dentro del espacio de integración.] }
        request_id: 77e6cf9e-6b7d-4c42-9bb4-937ec0c14d42
    PlanNotEligible:
      value:
        error:
          code: plan_not_eligible
          message: El plan no está disponible para las fechas, territorio, edades o cantidad de pasajeros indicada.
          details: { plan_id: ['108'] }
        request_id: 77e6cf9e-6b7d-4c42-9bb4-937ec0c14d42
    WebhookRegistered:
      value:
        endpoint:
          id: 14
          url: https://integracion.example.com/webhooks/portal-emision
          events: [order.created, order.updated, order.cancelled, order.issue_failed]
          is_active: true
        secret: whsec_MuestraSoloUnaVez_8P4x
    StandardReportedOrder:
      value:
        external_code: EXT-2026-000184
        plan_id: 108
        trip:
          origin_country: VE
          destination_territory_id: '4'
          starts_at: '2026-09-10'
          ends_at: '2026-09-20'
        emergency_contact:
          full_name: María Pérez
          email: maria@example.com
          phone: { country_code: '58', number: '4121234567' }
        passengers:
          - first_name: Juan
            last_name: Rodríguez
            birth_date: '1988-04-21'
            nationality: VE
            document: { type: passport, number: P12345678 }
            email: juan@example.com
            phone: { country_code: '58', number: '4121234567' }
            gender: M
            medical_conditions: null
        riders:
          - id: 12
            passenger_documents: [P12345678]
    BrazilReportedOrder:
      value:
        external_code: BR-2026-00991
        plan_id: 204
        trip:
          origin_country: BR
          destination_territory_id: '4'
          starts_at: '2026-10-02'
          ends_at: '2026-10-12'
        emergency_contact:
          full_name: Ana Souza
          email: ana@example.com
          phone: { country_code: '55', number: '11987654321' }
        passengers:
          - first_name: Carlos
            last_name: Souza
            birth_date: '1990-03-12'
            nationality: BR
            document: { type: cpf, number: '52998224725' }
            email: carlos@example.com
            phone: { country_code: '55', number: '11987654321' }
            gender: M
        brazil:
          has_cpf: true
          civil_status_id: 1
          address:
            cep: '01310100'
            street: Avenida Paulista
            number: '1000'
            complement: Apto 10
            neighborhood: Bela Vista
            city: São Paulo
            state: SP
