openapi: 3.1.0
info:
  title: TKS Suite — API de Órdenes Externas de Compra de Medios
  version: 1.2.0
  description: |
    API REST para la recepción, actualización, anulación y consulta de órdenes de compra
    de medios desde sistemas de gestión de agencias (ej. Medsys Star de Grupo Garnier)
    hacia la plataforma contable **TKS Suite**.

    ### Principios de Arquitectura e Integración
    - **Ingesta Desacoplada y Staging Contable:** Medsys Star transmite las órdenes operativas tal como se originan en la agencia. TKS Suite custodia íntegramente la información en `raw_payload` para auditoría y trazabilidad histórica.
    - **Auditoría Asíncrona en Staging:** La detección de discrepancias comerciales, datos auxiliares pendientes o ausencia de archivo PDF adjunto no interrumpe el flujo transaccional. La orden ingresa exitosamente a staging (HTTP 201/200) y se etiqueta automáticamente con `needs_review: true` y `review_reason` para la revisión del equipo contable.
    - **Contrato Mínimo de Integración:** La pasarela requiere un Bearer Token autorizado, una estructura JSON válida y el identificador `order.order_number`.
    - **Gestión Inmutable de Revisiones:** Cada actualización entrante se resguarda en el histórico inmutable. Revisiones con timestamp o numeración histórica se preservan en la bitácora con `applied: false` sin alterar el estado vigente.
    - **Capa de Ajustes Contables (Overrides):** Los ajustes de catálogo formalizados por contabilidad en TKS Suite residen en `accounting_overrides` y prevalecen autoritativamente al facturar sin ser sobreescritos por actualizaciones operativas.
    - **Bloqueo de Orden Facturada (Locked):** Tras la facturación formal de la orden en TKS Suite, la orden queda inmutable (`locked: true`).
    - **Gestión de Archivos de Soporte (PDF):** Soporte optimizado de hasta 5 MB en archivo binario original (~6.7 MB en Base64). Cargas que excedan los 10 MB del transporte HTTP deben retransmitirse con `"pdf": null` para asegurar el ingreso de la transacción a staging.
    - **Capacidad y Rendimiento:** Cuota de 120 peticiones por minuto en ventana deslizante de 60 segundos por compañía, con soporte de ráfaga completa (full burst) y cabeceras estándar `X-RateLimit-*`.

servers:
  - url: https://saas.tks.com.pa/functions/v1
    description: Servidor Oficial de Integración TKS Suite (Producción y Sandbox según Token)

security:
  - BearerAuth: []

paths:
  /external-orders:
    get:
      summary: Consultar estado de una orden y bloqueo contable
      description: |
        Permite al sistema origen consultar en tiempo real el estado de facturación de una orden.
        Si la orden ya fue facturada en TKS Suite, el endpoint retorna `locked: true`,
        el número de factura emitida y su código de control, permitiendo al sistema origen bloquear la edición local.
      operationId: getExternalOrderStatus
      parameters:
        - name: order_number
          in: query
          required: true
          description: Número de orden de compra a consultar (ej. "50209")
          schema:
            type: string
            example: "50209"
      responses:
        '200':
          description: Estado actual de la orden en TKS Suite
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderStatusResponse'
        '400':
          description: Falta el parámetro order_number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Token no autorizado para consultar órdenes de esta compañía o agencia
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Orden no encontrada en la compañía/ambiente del token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

    post:
      summary: Ingesta, actualización y anulación de órdenes de medios
      description: |
        Recibe eventos de creación (`ORDER_CREATED`), actualización (`ORDER_UPDATED`)
        o anulación (`ORDER_CANCELLED`) de órdenes de compra de medios.
        El único campo indispensable es `order.order_number`.
      operationId: postExternalOrder
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: Clave única para garantizar idempotencia en reintentos. Si se omite, se calcula `${order_number}-${rev}-${event}` y el hash SHA-256 del contenido del payload para diferenciar retransmisiones idénticas de actualizaciones de contenido.
          schema:
            type: string
            example: "50230-0-ORDER_CREATED"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalOrderPayload'
      responses:
        '201':
          description: "Orden registrada exitosamente por primera vez en staging (status: created)."
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSuccessResponse'
        '200':
          description: >
            Operación procesada exitosamente. El atributo `status` en el cuerpo de respuesta indica el resultado:
            `updated` (actualización aplicada al registro activo), `duplicate` (entrega idempotente confirmada sin cambios),
            `archived_historical_revision` (revisión histórica archivada con applied: false) o
            `revision_recorded_order_locked` (orden ya facturada; registrada en histórico y señalada para ajuste contable o Nota de Crédito).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSuccessResponse'
        '400':
          description: Estructura JSON no parseable o falta del campo obligatorio order.order_number
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Token sin permiso para la agencia especificada en el payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: "El cuerpo total de la solicitud supera el límite de transporte de 10 MB. La orden no se procesa en esta solicitud. El cliente debe reintentar omitiendo el archivo PDF o enviando pdf: null."
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Token API provisto por TKS Technology asignado a la compañía y ambiente.

  responses:
    Unauthorized:
      description: Token ausente, inválido o inactivo
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimitExceeded:
      description: Límite de tasa excedido (máximo 120 req/minuto)
      headers:
        Retry-After:
          schema:
            type: integer
          description: Segundos a esperar antes de reintentar
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitResponse'
    ServiceUnavailable:
      description: Error interno temporal del servicio
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: "Token inválido"

    RateLimitResponse:
      type: object
      required: [error, retry_after_seconds]
      properties:
        error:
          type: string
          example: "Límite de tasa excedido. Se permite un máximo de 120 peticiones por minuto."
        retry_after_seconds:
          type: integer
          example: 60

    ValidationErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Descripción del error de sintaxis o ausencia de identificador
          example: "El campo order.order_number es obligatorio para identificar la orden"
        field:
          type: [string, "null"]
          description: Nombre del campo faltante cuando aplica (null ante JSON mal formado)
          example: "order.order_number"

    OrderStatusResponse:
      type: object
      required:
        - order_number
        - billing_status
        - locked
        - needs_review
      properties:
        order_number:
          type: string
          example: "50230"
        billing_status:
          type: string
          enum: [pending, consolidated, invoiced, cancelled]
          description: Estado de facturación en TKS Suite (pending = pendiente, consolidated = agrupada, invoiced = facturada, cancelled = anulada)
          example: "pending"
        locked:
          type: boolean
          description: Indica si la orden ya fue facturada en el sistema contable y está protegida contra modificaciones
          example: false
        invoice_number:
          type: [string, "null"]
          example: "FE-01-0000000104"
        cufe:
          type: [string, "null"]
          example: "FE0120000000000-0000000000000000000000000000000000000000"
        invoiced_at:
          type: [string, "null"]
          format: date-time
        credit_status:
          type: string
          enum: [none, partial, total]
          example: "none"
        credited_amount:
          type: number
          example: 0
        stored_rev:
          type: integer
          example: 0
        needs_review:
          type: boolean
          description: Bandera que señala si contabilidad debe revisar alguna inconsistencia antes de facturar
          example: false
        review_reason:
          type: [string, "null"]
          example: null
        client_name:
          type: [string, "null"]
          description: Nombre o razón social del cliente preservado en staging
          example: "Arcos Dorados Panamá S.A."
        client_ruc:
          type: [string, "null"]
          description: RUC del cliente preservado en staging
          example: "155667788-2-2020"
        gross_cost:
          type: [number, "null"]
          description: Monto bruto de la orden preservado en staging
          example: 5000.00
        total:
          type: [number, "null"]
          description: Monto total con impuestos preservado en staging
          example: 5350.00

    OrderSuccessResponse:
      type: object
      required: [status, order_number, applied]
      properties:
        status:
          type: string
          enum: [created, updated, duplicate, archived_historical_revision, revision_recorded_order_locked]
          description: Resultado de la operación
          example: "created"
        id:
          type: string
          format: uuid
          example: "e2046e93-3257-4fbe-be35-38ba937ca11e"
        revision_id:
          type: string
          format: uuid
          example: "82242750-55c8-48b4-b4c8-50c359b4f3fe"
        order_number:
          type: string
          example: "50230"
        rev:
          type: integer
          description: Número de revisión de la orden, procesado y normalizado como entero por el servidor.
          example: 0
        stored_rev:
          type: integer
          description: Revisión activa almacenada actualmente en el espejo de TKS Suite (normalizada como entero).
          example: 0
        incoming_rev:
          type: integer
          description: Revisión recibida en la petición (normalizada como entero).
          example: 0
        applied:
          type: boolean
          description: false si la revisión fue archivada sin modificar la orden activa (ej. orden facturada o revisión histórica)
          example: true
        locked:
          type: boolean
          example: false
        needs_review:
          type: boolean
          example: false
        review_reason:
          type: [string, "null"]
          example: null
        change_class:
          type: string
          enum: [none, informational, fiscal]
          example: "none"
        changed_fields:
          type: array
          items:
            type: string
        pdf_stored:
          type: boolean
          example: true
        idempotency_key:
          type: string
          example: "50230-0-ORDER_CREATED"
        message:
          type: string
          example: "Orden creada exitosamente"
        environment:
          type: string
          example: "production"

    ExternalOrderPayload:
      type: object
      required:
        - order
      properties:
        event:
          type: string
          enum: [ORDER_CREATED, ORDER_UPDATED, ORDER_CANCELLED]
          description: >
            Tipo de evento de la transacción. Cuando event es `ORDER_CANCELLED`, los bloques auxiliares
            (`budget`, `client`, `financials`, `items`, etc.) son completamente opcionales, requiriéndose
            únicamente el identificador `order.order_number` para procesar la solicitud de anulación.
            Por defecto `ORDER_CREATED`.
          example: "ORDER_CREATED"
        sent_at:
          type: string
          format: date-time
          example: "2026-10-20T18:00:00Z"
        agency:
          type: object
          description: Datos de la agencia. Si se omite code, se asigna automáticamente la agencia del Bearer Token.
          properties:
            code:
              type: string
              description: Código de la agencia (ej. OMD, BBDO, PHD). Si se omite, asume la del token.
              example: "OMD"
            name:
              type: string
              example: "OMD Centroamérica"
        budget:
          type: object
          properties:
            code:
              type: string
              example: "BUD-2026-OMD-009"
            rev:
              oneOf:
                - type: integer
                - type: string
              description: Número de versión o revisión del presupuesto/orden originado en Medsys Star (ej. 0, 1, 2...). TKS Suite utiliza este valor para versionar las revisiones de la orden en el histórico inmutable. Se admite formato entero o string numérico (ej. 0 o "0"); el servidor lo procesa y devuelve normalizado como entero en sus respuestas.
              default: 0
              example: 0
            date:
              type: string
              example: "2026-10-20"
            campaign:
              type: string
              example: "Campaña Navideña McCafé 2026"
            brand:
              type: string
              example: "McCafé"
            period:
              type: object
              properties:
                start:
                  type: string
                  example: "2026-11-01"
                end:
                  type: string
                  example: "2026-12-31"
        client:
          type: object
          properties:
            code:
              type: string
              example: "CLI-MCD-01"
            name:
              type: string
              example: "Arcos Dorados Panamá S.A."
            ruc:
              type: string
              description: RUC del cliente. Se acepta cualquier formato o texto; si difiere del estándar se señala en needs_review sin rechazar.
              example: "155667788-2-2020"
            dv:
              type: string
              description: Dígito Verificador (opcional).
              example: "45"
            email:
              type: string
              description: Correo electrónico del cliente. Acepta cualquier texto o cadena vacía sin validación restrictiva.
              example: "facturas.panama@arcosdorados.com"
            payment_terms_days:
              oneOf:
                - type: integer
                - type: string
              description: Días de crédito del cliente
              example: 30
        order:
          type: object
          required:
            - order_number
          properties:
            order_number:
              type: string
              description: Identificador de la orden de medios en Medsys (ÚNICO CAMPO OBLIGATORIO)
              example: "50230"
            status:
              type: string
              description: Estado en Medsys (texto libre, ej. ACTIVA, MODIFICADA, ANULADA)
              default: "ACTIVA"
              example: "ACTIVA"
            updated_at:
              type: string
              example: "2026-10-20T18:00:00Z"
            concept:
              type: string
              description: Concepto de la orden (texto libre, ej. Compra, Agency Fee, etc.)
              default: "Compra"
              example: "Compra"
            media_type:
              type: string
              example: "TELEVISION"
            submedia_type:
              type: string
              example: "SPOT PRIME TIME"
            provider:
              type: object
              properties:
                code:
                  type: string
                  example: "PROV-MEDCOM"
                name:
                  type: string
                  example: "Corporación Medcom Panamá S.A."
                ruc:
                  type: string
                  example: "45678-1-123456"
                dv:
                  type: string
                  example: "08"
                email:
                  type: string
                  example: "facturacion@medcom.com.pa"
            items:
              type: array
              description: Desglose de múltiples servicios (para órdenes multi-línea)
              items:
                type: object
                properties:
                  service_description:
                    type: string
                    example: "Transmisión 15 Spots 30s Prime Time - Telemetro"
                  purchase_format:
                    type: string
                    example: "ROTATIVO PRIME"
                  version:
                    type: string
                    example: "Spot 30s V1"
                  ad_units:
                    oneOf:
                      - type: number
                      - type: string
                    example: 15
                  unit_cost:
                    oneOf:
                      - type: number
                      - type: string
                    example: 200.00
                  gross_cost:
                    oneOf:
                      - type: number
                      - type: string
                    example: 3000.00
                  itbms_pct:
                    oneOf:
                      - type: number
                      - type: string
                    example: 7.00
            item_detail:
              type: object
              description: Detalle para órdenes con un único servicio (alternativa a items)
              properties:
                service_description:
                  type: string
                  example: "Transmisión 20 Spots 30s Prime Time Telemetro y RPC - McCafé"
                purchase_format:
                  type: string
                  example: "ROTATIVO"
                version:
                  type: string
                  example: "Spot Navidad 30s V1"
                ad_units:
                  oneOf:
                    - type: number
                    - type: string
                  example: 20
                unit_cost:
                  oneOf:
                    - type: number
                    - type: string
                  example: 250.00
            financials:
              type: object
              description: Desglose financiero. Acepta números o strings numéricos. Si los valores difieren de la suma, se acepta igualmente y se marca needs_review.
              properties:
                currency:
                  type: string
                  default: USD
                  example: USD
                gross_cost:
                  oneOf:
                    - type: number
                    - type: string
                  example: 5000.00
                agency_fee:
                  oneOf:
                    - type: number
                    - type: string
                  example: 0.00
                itbms_pct:
                  oneOf:
                    - type: number
                    - type: string
                  description: Porcentaje de ITBMS (opcional; ej. 7.00, o 0 si la orden está exenta o no aplica impuesto).
                  example: 7.00
                itbms_amount:
                  oneOf:
                    - type: number
                    - type: string
                  description: Monto de ITBMS (opcional; 0 si no aplica impuesto).
                  example: 350.00
                total:
                  oneOf:
                    - type: number
                    - type: string
                  description: Importe total de la orden liquidado por Medsys Star. Se almacena exactamente con el valor recibido.
                  example: 5350.00
            personnel:
              type: object
              properties:
                planner:
                  type: string
                  example: "Carlos Medina"
                buyer:
                  type: string
                  example: "Valeria Rios"
                account_director:
                  type: string
                  example: "Marissa Solano"
            custom_fields:
              type: object
              description: Campos adicionales o personalizados del sistema de origen
              properties:
                product_code:
                  type: string
                  example: "MCCAFE-NOV-DIC"
                cost_center:
                  type: string
                  example: "CC-PAN-MKT-01"
                contract_ref:
                  type: string
                  example: "MEDCOM-2026-998"
            replaces_order_number:
              type: [string, "null"]
              example: null
            pdf:
              type: [object, "null"]
              required:
                - filename
                - content_base64
              description: "Documento de respaldo. Límite recomendado de 5 MB binarios (~6.7 MB Base64). Si el PDF supera 5 MB pero el payload total no excede 10 MB, la orden ingresa a staging con pdf_stored: false y needs_review: true. Si el payload excede 10 MB, la pasarela HTTP retorna 413."
              properties:
                filename:
                  type: string
                  example: "OC-50230-McCafe-OMD.pdf"
                content_base64:
                  type: string
                  description: Cadena en Base64 del archivo PDF generado por el sistema de origen.
