asyncapi: 3.0.0
info:
  title: AndesRetail Interop Events
  version: 1.0.0
  description: |
    Eventos y comandos internos de la capa de interoperabilidad sobre Kafka.
    Sobre CloudEvents 1.0 en modo binario: los atributos viajan como cabeceras
    y el payload de cada mensaje es el campo data.
    Cada canal declara en bindings.kafka la configuración de su tópico con los
    valores de producción; es la única fuente para crearlos (ADR-017). En local
    la herramienta usa factor de replicación 1. Cada operación de recepción
    declara su grupo consumidor en bindings.kafka.groupId.
    Diseño en docs/to-be/06-eventos.md.
defaultContentType: application/json
servers:
  local:
    host: localhost:9092
    protocol: kafka
    description: Contenedor local
  demo:
    host: confluent-cloud.example:9092
    protocol: kafka-secure
    description: Confluent Cloud Basic. El host real se toma del gestor de secretos
channels:
  rawErpChanges:
    address: raw.erp.changes
    description: Cambios crudos del ERP, sin traducir. Interno del erp-adapter y no estable; ningún otro servicio lo lee. Clave tabla y llave
    messages:
      RawErpChange: { $ref: '#/components/messages/RawErpChange' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: raw.erp.changes
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  rawWmsChanges:
    address: raw.wms.changes
    description: Cambios crudos del WMS, sin traducir. Interno del wms-adapter y no estable; ningún otro servicio lo lee. Clave sku y ubicación
    messages:
      RawWmsChange: { $ref: '#/components/messages/RawWmsChange' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: raw.wms.changes
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  catalogProducts:
    address: andes.catalog.products.v1
    description: Estado de producto. Compactado. Clave sku
    messages:
      ProductChanged: { $ref: '#/components/messages/ProductChanged' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.catalog.products.v1
        partitions: 12
        replicas: 3
        topicConfiguration:
          cleanup.policy: [compact]
          min.insync.replicas: 2
  pricingPrices:
    address: andes.pricing.prices.v1
    description: Estado de precio. Compactado. Clave sku|channel|priceList; el valor lleva el calendario completo del alcance, así la compactación no borra el precio vigente cuando llega uno futuro (ADR-020). Conserva al menos una hora de versiones antes de compactar (min.compaction.lag.ms), para que la vista de precios de order-service reconstruya su historial dentro de la ventana (ADR-015)
    messages:
      PriceChanged: { $ref: '#/components/messages/PriceChanged' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.pricing.prices.v1
        partitions: 12
        replicas: 3
        topicConfiguration:
          cleanup.policy: [compact]
          min.compaction.lag.ms: 3600000
          min.insync.replicas: 2
  inventoryStockLevels:
    address: andes.inventory.stock-levels.v1
    description: Stock físico según WMS. Compactado. Clave sku|locationId
    messages:
      StockLevelChanged: { $ref: '#/components/messages/StockLevelChanged' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.inventory.stock-levels.v1
        partitions: 12
        replicas: 3
        topicConfiguration:
          cleanup.policy: [compact]
          min.insync.replicas: 2
  sourcesSync:
    address: andes.sources.sync.v1
    description: |
      Marca de sincronización de cada sistema de origen (ADR-018). Compactado. Clave system.
      Dice hasta qué posición del feed del legado se publicaron todos los cambios y cuándo se
      consultó con éxito. Con ella pricing e inventory calculan asOf ("vigente hasta") y stale.
    messages:
      SourceSynced: { $ref: '#/components/messages/SourceSynced' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.sources.sync.v1
        partitions: 1
        replicas: 3
        topicConfiguration:
          cleanup.policy: [compact]
          min.insync.replicas: 2
  inventoryReservations:
    address: andes.inventory.reservations.v1
    description: Hechos de reservas. Clave orderId
    messages:
      StockReserved: { $ref: '#/components/messages/StockReserved' }
      ReservationConfirmed: { $ref: '#/components/messages/ReservationConfirmed' }
      ReservationReleased: { $ref: '#/components/messages/ReservationReleased' }
      ReservationExpired: { $ref: '#/components/messages/ReservationExpired' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.inventory.reservations.v1
        partitions: 6
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 1209600000
          min.insync.replicas: 2
  ordersEvents:
    address: andes.orders.events.v1
    description: Hechos de órdenes. Clave orderId
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1
        partitions: 6
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  paymentsCommands:
    address: andes.payments.commands.v1
    description: Comandos a payment-service. Clave orderId
    messages:
      PaymentRequested: { $ref: '#/components/messages/PaymentRequested' }
      RefundRequested: { $ref: '#/components/messages/RefundRequested' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.commands.v1
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  paymentsEvents:
    address: andes.payments.events.v1
    description: Hechos de pagos. Clave orderId
    messages:
      PaymentApproved: { $ref: '#/components/messages/PaymentApproved' }
      PaymentRejected: { $ref: '#/components/messages/PaymentRejected' }
      PaymentRefunded: { $ref: '#/components/messages/PaymentRefunded' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.events.v1
        partitions: 6
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  omsCommands:
    address: andes.oms.commands.v1
    description: Comandos al adaptador del OMS. Clave orderId
    messages:
      CreateOmsOrder: { $ref: '#/components/messages/CreateOmsOrder' }
      CancelOmsOrder: { $ref: '#/components/messages/CancelOmsOrder' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.oms.commands.v1
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  omsEvents:
    address: andes.oms.events.v1
    description: Respuestas del adaptador del OMS. Clave orderId
    messages:
      OmsOrderCreated: { $ref: '#/components/messages/OmsOrderCreated' }
      OmsOrderFailed: { $ref: '#/components/messages/OmsOrderFailed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.oms.events.v1
        partitions: 6
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 1209600000
          min.insync.replicas: 2
  fulfillmentEvents:
    address: andes.fulfillment.events.v1
    description: Hechos de despacho desde el WMS. Clave orderId
    messages:
      DispatchStarted: { $ref: '#/components/messages/DispatchStarted' }
      DeliveryConfirmed: { $ref: '#/components/messages/DeliveryConfirmed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.fulfillment.events.v1
        partitions: 6
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  audit:
    address: andes.audit.v1
    description: Registro de auditoría. Clave correlationId. Sin vencimiento en Kafka; el archivo con bloqueo contra modificación vive en almacenamiento de objetos (10-observabilidad.md)
    messages:
      AuditRecorded: { $ref: '#/components/messages/AuditRecorded' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.audit.v1
        partitions: 6
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: -1
          min.insync.replicas: 2
  inventoryReservationsOrderServiceRetry:
    address: andes.inventory.reservations.v1.order-service.retry
    description: Reintentos de order-service sobre andes.inventory.reservations.v1. Lleva el mensaje original sin cambios en su payload; order-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      StockReserved: { $ref: '#/components/messages/StockReserved' }
      ReservationConfirmed: { $ref: '#/components/messages/ReservationConfirmed' }
      ReservationReleased: { $ref: '#/components/messages/ReservationReleased' }
      ReservationExpired: { $ref: '#/components/messages/ReservationExpired' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.inventory.reservations.v1.order-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  inventoryReservationsOrderServiceDlq:
    address: andes.inventory.reservations.v1.order-service.dlq
    description: DLQ de order-service sobre andes.inventory.reservations.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      StockReserved: { $ref: '#/components/messages/StockReserved' }
      ReservationConfirmed: { $ref: '#/components/messages/ReservationConfirmed' }
      ReservationReleased: { $ref: '#/components/messages/ReservationReleased' }
      ReservationExpired: { $ref: '#/components/messages/ReservationExpired' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.inventory.reservations.v1.order-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  ordersEventsWebhookServiceRetry:
    address: andes.orders.events.v1.webhook-service.retry
    description: Reintentos de webhook-service sobre andes.orders.events.v1. Lleva el mensaje original sin cambios en su payload; webhook-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1.webhook-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  ordersEventsWebhookServiceDlq:
    address: andes.orders.events.v1.webhook-service.dlq
    description: DLQ de webhook-service sobre andes.orders.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1.webhook-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  ordersEventsOmsAdapterRetry:
    address: andes.orders.events.v1.oms-adapter.retry
    description: Reintentos de oms-adapter sobre andes.orders.events.v1. Lleva el mensaje original sin cambios en su payload; oms-adapter lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1.oms-adapter.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  ordersEventsOmsAdapterDlq:
    address: andes.orders.events.v1.oms-adapter.dlq
    description: DLQ de oms-adapter sobre andes.orders.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1.oms-adapter.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  ordersEventsCrmAdapterRetry:
    address: andes.orders.events.v1.crm-adapter.retry
    description: Reintentos de crm-adapter sobre andes.orders.events.v1. Lleva el mensaje original sin cambios en su payload; crm-adapter lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1.crm-adapter.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  ordersEventsCrmAdapterDlq:
    address: andes.orders.events.v1.crm-adapter.dlq
    description: DLQ de crm-adapter sobre andes.orders.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      OrderCreated: { $ref: '#/components/messages/OrderCreated' }
      OrderConfirmed: { $ref: '#/components/messages/OrderConfirmed' }
      OrderCancelled: { $ref: '#/components/messages/OrderCancelled' }
      OrderDispatched: { $ref: '#/components/messages/OrderDispatched' }
      OrderDelivered: { $ref: '#/components/messages/OrderDelivered' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.orders.events.v1.crm-adapter.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  paymentsCommandsPaymentServiceRetry:
    address: andes.payments.commands.v1.payment-service.retry
    description: Reintentos de payment-service sobre andes.payments.commands.v1. Lleva el mensaje original sin cambios en su payload; payment-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      PaymentRequested: { $ref: '#/components/messages/PaymentRequested' }
      RefundRequested: { $ref: '#/components/messages/RefundRequested' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.commands.v1.payment-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  paymentsCommandsPaymentServiceDlq:
    address: andes.payments.commands.v1.payment-service.dlq
    description: DLQ de payment-service sobre andes.payments.commands.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md). Cada mensaje es el original salvo los datos sensibles, que llegan enmascarados con el valor fijo --data_redacted-- y listados en la cabecera andes_payload_redacted (ADR-029). Un PaymentRequested llega con payment.token enmascarado y no sirve para volver a cobrar, porque su orden ya venció
    messages:
      PaymentRequestedMasked: { $ref: '#/components/messages/PaymentRequestedMasked' }
      RefundRequested: { $ref: '#/components/messages/RefundRequested' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.commands.v1.payment-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  paymentsEventsOrderServiceRetry:
    address: andes.payments.events.v1.order-service.retry
    description: Reintentos de order-service sobre andes.payments.events.v1. Lleva el mensaje original sin cambios en su payload; order-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      PaymentApproved: { $ref: '#/components/messages/PaymentApproved' }
      PaymentRejected: { $ref: '#/components/messages/PaymentRejected' }
      PaymentRefunded: { $ref: '#/components/messages/PaymentRefunded' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.events.v1.order-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  paymentsEventsOrderServiceDlq:
    address: andes.payments.events.v1.order-service.dlq
    description: DLQ de order-service sobre andes.payments.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      PaymentApproved: { $ref: '#/components/messages/PaymentApproved' }
      PaymentRejected: { $ref: '#/components/messages/PaymentRejected' }
      PaymentRefunded: { $ref: '#/components/messages/PaymentRefunded' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.events.v1.order-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  paymentsEventsWebhookServiceRetry:
    address: andes.payments.events.v1.webhook-service.retry
    description: Reintentos de webhook-service sobre andes.payments.events.v1. Lleva el mensaje original sin cambios en su payload; webhook-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      PaymentApproved: { $ref: '#/components/messages/PaymentApproved' }
      PaymentRejected: { $ref: '#/components/messages/PaymentRejected' }
      PaymentRefunded: { $ref: '#/components/messages/PaymentRefunded' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.events.v1.webhook-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  paymentsEventsWebhookServiceDlq:
    address: andes.payments.events.v1.webhook-service.dlq
    description: DLQ de webhook-service sobre andes.payments.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      PaymentApproved: { $ref: '#/components/messages/PaymentApproved' }
      PaymentRejected: { $ref: '#/components/messages/PaymentRejected' }
      PaymentRefunded: { $ref: '#/components/messages/PaymentRefunded' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.payments.events.v1.webhook-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  omsCommandsOmsAdapterRetry:
    address: andes.oms.commands.v1.oms-adapter.retry
    description: Reintentos de oms-adapter sobre andes.oms.commands.v1. Lleva el mensaje original sin cambios en su payload; oms-adapter lo vuelve a procesar cuando vence la espera. En el registro en OMS los reintentos siguen hasta 24 horas antes de ir a DLQ (09-resiliencia.md)
    messages:
      CreateOmsOrder: { $ref: '#/components/messages/CreateOmsOrder' }
      CancelOmsOrder: { $ref: '#/components/messages/CancelOmsOrder' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.oms.commands.v1.oms-adapter.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  omsCommandsOmsAdapterDlq:
    address: andes.oms.commands.v1.oms-adapter.dlq
    description: DLQ de oms-adapter sobre andes.oms.commands.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      CreateOmsOrder: { $ref: '#/components/messages/CreateOmsOrder' }
      CancelOmsOrder: { $ref: '#/components/messages/CancelOmsOrder' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.oms.commands.v1.oms-adapter.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  omsEventsOrderServiceRetry:
    address: andes.oms.events.v1.order-service.retry
    description: Reintentos de order-service sobre andes.oms.events.v1. Lleva el mensaje original sin cambios en su payload; order-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      OmsOrderCreated: { $ref: '#/components/messages/OmsOrderCreated' }
      OmsOrderFailed: { $ref: '#/components/messages/OmsOrderFailed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.oms.events.v1.order-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  omsEventsOrderServiceDlq:
    address: andes.oms.events.v1.order-service.dlq
    description: DLQ de order-service sobre andes.oms.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      OmsOrderCreated: { $ref: '#/components/messages/OmsOrderCreated' }
      OmsOrderFailed: { $ref: '#/components/messages/OmsOrderFailed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.oms.events.v1.order-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  fulfillmentEventsOrderServiceRetry:
    address: andes.fulfillment.events.v1.order-service.retry
    description: Reintentos de order-service sobre andes.fulfillment.events.v1. Lleva el mensaje original sin cambios en su payload; order-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      DispatchStarted: { $ref: '#/components/messages/DispatchStarted' }
      DeliveryConfirmed: { $ref: '#/components/messages/DeliveryConfirmed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.fulfillment.events.v1.order-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  fulfillmentEventsOrderServiceDlq:
    address: andes.fulfillment.events.v1.order-service.dlq
    description: DLQ de order-service sobre andes.fulfillment.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      DispatchStarted: { $ref: '#/components/messages/DispatchStarted' }
      DeliveryConfirmed: { $ref: '#/components/messages/DeliveryConfirmed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.fulfillment.events.v1.order-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
  fulfillmentEventsInventoryServiceRetry:
    address: andes.fulfillment.events.v1.inventory-service.retry
    description: Reintentos de inventory-service sobre andes.fulfillment.events.v1. Lleva el mensaje original sin cambios en su payload; inventory-service lo vuelve a procesar cuando vence la espera. Esperas de 1 minuto, 10 minutos y 1 hora (09-resiliencia.md)
    messages:
      DispatchStarted: { $ref: '#/components/messages/DispatchStarted' }
      DeliveryConfirmed: { $ref: '#/components/messages/DeliveryConfirmed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.fulfillment.events.v1.inventory-service.retry
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 604800000
          min.insync.replicas: 2
  fulfillmentEventsInventoryServiceDlq:
    address: andes.fulfillment.events.v1.inventory-service.dlq
    description: DLQ de inventory-service sobre andes.fulfillment.events.v1. Mensajes que agotaron los reintentos o fallaron por un error no transitorio, que puede ser no cumplir el esquema. Tiene alerta y se reprocesa con el rol ops-admin (09-resiliencia.md)
    messages:
      DispatchStarted: { $ref: '#/components/messages/DispatchStarted' }
      DeliveryConfirmed: { $ref: '#/components/messages/DeliveryConfirmed' }
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        topic: andes.fulfillment.events.v1.inventory-service.dlq
        partitions: 3
        replicas: 3
        topicConfiguration:
          cleanup.policy: [delete]
          retention.ms: 2592000000
          min.insync.replicas: 2
operations:
  publishRawErpChange:
    action: send
    x-producers: [erp-adapter]
    channel: { $ref: '#/channels/rawErpChanges' }
    summary: erp-adapter guarda cada cambio leído del feed del ERP antes de traducirlo
  erpAdapterReceivesRawErpChanges:
    action: receive
    channel: { $ref: '#/channels/rawErpChanges' }
    summary: erp-adapter traduce el cambio al modelo canónico
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [erp-adapter] }
  publishRawWmsChange:
    action: send
    x-producers: [wms-adapter]
    channel: { $ref: '#/channels/rawWmsChanges' }
    summary: wms-adapter guarda cada movimiento leído del WMS antes de traducirlo
  wmsAdapterReceivesRawWmsChanges:
    action: receive
    channel: { $ref: '#/channels/rawWmsChanges' }
    summary: wms-adapter traduce el movimiento al modelo canónico
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [wms-adapter] }
  publishProductChanged:
    action: send
    x-producers: [erp-adapter]
    channel: { $ref: '#/channels/catalogProducts' }
    summary: erp-adapter publica cambios de producto
  pricingServiceReceivesCatalogProducts:
    action: receive
    channel: { $ref: '#/channels/catalogProducts' }
    summary: pricing-service actualiza el modelo de lectura de producto
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [pricing-service] }
  orderServiceReceivesCatalogProducts:
    action: receive
    channel: { $ref: '#/channels/catalogProducts' }
    summary: order-service conoce los SKU habilitados por canal para validar la orden
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  inventoryServiceReceivesCatalogProducts:
    action: receive
    channel: { $ref: '#/channels/catalogProducts' }
    summary: inventory-service conoce el surtido de cada canal para responder en notFound los SKU no habilitados (ADR-019)
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [inventory-service] }
  publishPriceChanged:
    action: send
    x-producers: [erp-adapter]
    channel: { $ref: '#/channels/pricingPrices' }
    summary: erp-adapter publica cambios de precio
  pricingServiceReceivesPricingPrices:
    action: receive
    channel: { $ref: '#/channels/pricingPrices' }
    summary: pricing-service actualiza el modelo de lectura
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [pricing-service] }
  orderServiceReceivesPricingPrices:
    action: receive
    channel: { $ref: '#/channels/pricingPrices' }
    summary: order-service guarda su vista de precios con historial para validar el precio de la orden (ADR-015)
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  publishStockLevelChanged:
    action: send
    x-producers: [wms-adapter]
    channel: { $ref: '#/channels/inventoryStockLevels' }
    summary: wms-adapter publica el stock físico
  inventoryServiceReceivesInventoryStockLevels:
    action: receive
    channel: { $ref: '#/channels/inventoryStockLevels' }
    summary: inventory-service actualiza onHand y el modelo de lectura
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [inventory-service] }
  erpAdapterPublishesSourceSynced:
    action: send
    x-producers: [erp-adapter]
    channel: { $ref: '#/channels/sourcesSync' }
    summary: erp-adapter publica su marca tras cada sondeo exitoso, como máximo cada 15 segundos y solo con los cambios previos confirmados por Kafka
  wmsAdapterPublishesSourceSynced:
    action: send
    x-producers: [wms-adapter]
    channel: { $ref: '#/channels/sourcesSync' }
    summary: wms-adapter publica su marca tras cada sondeo exitoso, como máximo cada 15 segundos y solo con los cambios previos confirmados por Kafka
  pricingServiceReceivesSourcesSync:
    action: receive
    channel: { $ref: '#/channels/sourcesSync' }
    summary: pricing-service toma la marca del ERP para calcular asOf y stale
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [pricing-service] }
  inventoryServiceReceivesSourcesSync:
    action: receive
    channel: { $ref: '#/channels/sourcesSync' }
    summary: inventory-service toma la marca del WMS para calcular asOf y stale
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [inventory-service] }
  publishReservationEvents:
    action: send
    x-producers: [inventory-service]
    channel: { $ref: '#/channels/inventoryReservations' }
    summary: inventory-service publica hechos de reservas
  orderServiceReceivesInventoryReservations:
    action: receive
    channel: { $ref: '#/channels/inventoryReservations' }
    summary: order-service cancela órdenes cuya reserva venció
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  publishOrderEvents:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/ordersEvents' }
    summary: order-service publica hechos de órdenes mediante outbox
  webhookServiceReceivesOrdersEvents:
    action: receive
    channel: { $ref: '#/channels/ordersEvents' }
    summary: webhook-service notifica a los partners suscritos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [webhook-service] }
  omsAdapterReceivesOrdersEvents:
    action: receive
    channel: { $ref: '#/channels/ordersEvents' }
    summary: oms-adapter sincroniza con el OMS los cambios de la orden
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [oms-adapter] }
  crmAdapterReceivesOrdersEvents:
    action: receive
    channel: { $ref: '#/channels/ordersEvents' }
    summary: crm-adapter informa la compra al CRM
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [crm-adapter] }
  sendPaymentCommands:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/paymentsCommands' }
    summary: order-service pide cobro o reembolso
  paymentServiceReceivesPaymentsCommands:
    action: receive
    channel: { $ref: '#/channels/paymentsCommands' }
    summary: payment-service ejecuta contra la pasarela
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [payment-service] }
  publishPaymentEvents:
    action: send
    x-producers: [payment-service]
    channel: { $ref: '#/channels/paymentsEvents' }
    summary: payment-service publica el resultado del cobro
  orderServiceReceivesPaymentsEvents:
    action: receive
    channel: { $ref: '#/channels/paymentsEvents' }
    summary: order-service avanza o compensa la saga
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  webhookServiceReceivesPaymentsEvents:
    action: receive
    channel: { $ref: '#/channels/paymentsEvents' }
    summary: webhook-service notifica el resultado del pago
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [webhook-service] }
  sendOmsCommands:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/omsCommands' }
    summary: order-service pide registrar o cancelar en el OMS
  omsAdapterReceivesOmsCommands:
    action: receive
    channel: { $ref: '#/channels/omsCommands' }
    summary: oms-adapter ejecuta con límite de concurrencia
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [oms-adapter] }
  publishOmsEvents:
    action: send
    x-producers: [oms-adapter]
    channel: { $ref: '#/channels/omsEvents' }
    summary: oms-adapter informa el resultado
  orderServiceReceivesOmsEvents:
    action: receive
    channel: { $ref: '#/channels/omsEvents' }
    summary: order-service registra en la orden el resultado del OMS
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  publishFulfillmentEvents:
    action: send
    x-producers: [wms-adapter]
    channel: { $ref: '#/channels/fulfillmentEvents' }
    summary: wms-adapter publica despacho y entrega
  orderServiceReceivesFulfillmentEvents:
    action: receive
    channel: { $ref: '#/channels/fulfillmentEvents' }
    summary: order-service cambia el estado de la orden
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  inventoryServiceReceivesFulfillmentEvents:
    action: receive
    channel: { $ref: '#/channels/fulfillmentEvents' }
    summary: inventory-service consume la reserva
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [inventory-service] }
  publishAudit:
    action: send
    x-producers: [erp-adapter, wms-adapter, oms-adapter, crm-adapter, pricing-service, inventory-service, order-service, payment-service, webhook-service]
    channel: { $ref: '#/channels/audit' }
    summary: Todos los servicios registran auditoría
  orderServiceSendsToInventoryReservationsOrderServiceRetry:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/inventoryReservationsOrderServiceRetry' }
    summary: order-service aparta a reintento lo que falló, sin bloquear la partición
  orderServiceReceivesInventoryReservationsOrderServiceRetry:
    action: receive
    channel: { $ref: '#/channels/inventoryReservationsOrderServiceRetry' }
    summary: order-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  orderServiceSendsToInventoryReservationsOrderServiceDlq:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/inventoryReservationsOrderServiceDlq' }
    summary: order-service aparta a DLQ lo que agotó los reintentos
  webhookServiceSendsToOrdersEventsWebhookServiceRetry:
    action: send
    x-producers: [webhook-service]
    channel: { $ref: '#/channels/ordersEventsWebhookServiceRetry' }
    summary: webhook-service aparta a reintento lo que falló, sin bloquear la partición
  webhookServiceReceivesOrdersEventsWebhookServiceRetry:
    action: receive
    channel: { $ref: '#/channels/ordersEventsWebhookServiceRetry' }
    summary: webhook-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [webhook-service] }
  webhookServiceSendsToOrdersEventsWebhookServiceDlq:
    action: send
    x-producers: [webhook-service]
    channel: { $ref: '#/channels/ordersEventsWebhookServiceDlq' }
    summary: webhook-service aparta a DLQ lo que agotó los reintentos
  omsAdapterSendsToOrdersEventsOmsAdapterRetry:
    action: send
    x-producers: [oms-adapter]
    channel: { $ref: '#/channels/ordersEventsOmsAdapterRetry' }
    summary: oms-adapter aparta a reintento lo que falló, sin bloquear la partición
  omsAdapterReceivesOrdersEventsOmsAdapterRetry:
    action: receive
    channel: { $ref: '#/channels/ordersEventsOmsAdapterRetry' }
    summary: oms-adapter reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [oms-adapter] }
  omsAdapterSendsToOrdersEventsOmsAdapterDlq:
    action: send
    x-producers: [oms-adapter]
    channel: { $ref: '#/channels/ordersEventsOmsAdapterDlq' }
    summary: oms-adapter aparta a DLQ lo que agotó los reintentos
  crmAdapterSendsToOrdersEventsCrmAdapterRetry:
    action: send
    x-producers: [crm-adapter]
    channel: { $ref: '#/channels/ordersEventsCrmAdapterRetry' }
    summary: crm-adapter aparta a reintento lo que falló, sin bloquear la partición
  crmAdapterReceivesOrdersEventsCrmAdapterRetry:
    action: receive
    channel: { $ref: '#/channels/ordersEventsCrmAdapterRetry' }
    summary: crm-adapter reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [crm-adapter] }
  crmAdapterSendsToOrdersEventsCrmAdapterDlq:
    action: send
    x-producers: [crm-adapter]
    channel: { $ref: '#/channels/ordersEventsCrmAdapterDlq' }
    summary: crm-adapter aparta a DLQ lo que agotó los reintentos
  paymentServiceSendsToPaymentsCommandsPaymentServiceRetry:
    action: send
    x-producers: [payment-service]
    channel: { $ref: '#/channels/paymentsCommandsPaymentServiceRetry' }
    summary: payment-service aparta a reintento lo que falló, sin bloquear la partición
  paymentServiceReceivesPaymentsCommandsPaymentServiceRetry:
    action: receive
    channel: { $ref: '#/channels/paymentsCommandsPaymentServiceRetry' }
    summary: payment-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [payment-service] }
  paymentServiceSendsToPaymentsCommandsPaymentServiceDlq:
    action: send
    x-producers: [payment-service]
    channel: { $ref: '#/channels/paymentsCommandsPaymentServiceDlq' }
    summary: payment-service aparta a DLQ lo que agotó los reintentos
  orderServiceSendsToPaymentsEventsOrderServiceRetry:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/paymentsEventsOrderServiceRetry' }
    summary: order-service aparta a reintento lo que falló, sin bloquear la partición
  orderServiceReceivesPaymentsEventsOrderServiceRetry:
    action: receive
    channel: { $ref: '#/channels/paymentsEventsOrderServiceRetry' }
    summary: order-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  orderServiceSendsToPaymentsEventsOrderServiceDlq:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/paymentsEventsOrderServiceDlq' }
    summary: order-service aparta a DLQ lo que agotó los reintentos
  webhookServiceSendsToPaymentsEventsWebhookServiceRetry:
    action: send
    x-producers: [webhook-service]
    channel: { $ref: '#/channels/paymentsEventsWebhookServiceRetry' }
    summary: webhook-service aparta a reintento lo que falló, sin bloquear la partición
  webhookServiceReceivesPaymentsEventsWebhookServiceRetry:
    action: receive
    channel: { $ref: '#/channels/paymentsEventsWebhookServiceRetry' }
    summary: webhook-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [webhook-service] }
  webhookServiceSendsToPaymentsEventsWebhookServiceDlq:
    action: send
    x-producers: [webhook-service]
    channel: { $ref: '#/channels/paymentsEventsWebhookServiceDlq' }
    summary: webhook-service aparta a DLQ lo que agotó los reintentos
  omsAdapterSendsToOmsCommandsOmsAdapterRetry:
    action: send
    x-producers: [oms-adapter]
    channel: { $ref: '#/channels/omsCommandsOmsAdapterRetry' }
    summary: oms-adapter aparta a reintento lo que falló, sin bloquear la partición
  omsAdapterReceivesOmsCommandsOmsAdapterRetry:
    action: receive
    channel: { $ref: '#/channels/omsCommandsOmsAdapterRetry' }
    summary: oms-adapter reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [oms-adapter] }
  omsAdapterSendsToOmsCommandsOmsAdapterDlq:
    action: send
    x-producers: [oms-adapter]
    channel: { $ref: '#/channels/omsCommandsOmsAdapterDlq' }
    summary: oms-adapter aparta a DLQ lo que agotó los reintentos
  orderServiceSendsToOmsEventsOrderServiceRetry:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/omsEventsOrderServiceRetry' }
    summary: order-service aparta a reintento lo que falló, sin bloquear la partición
  orderServiceReceivesOmsEventsOrderServiceRetry:
    action: receive
    channel: { $ref: '#/channels/omsEventsOrderServiceRetry' }
    summary: order-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  orderServiceSendsToOmsEventsOrderServiceDlq:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/omsEventsOrderServiceDlq' }
    summary: order-service aparta a DLQ lo que agotó los reintentos
  orderServiceSendsToFulfillmentEventsOrderServiceRetry:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/fulfillmentEventsOrderServiceRetry' }
    summary: order-service aparta a reintento lo que falló, sin bloquear la partición
  orderServiceReceivesFulfillmentEventsOrderServiceRetry:
    action: receive
    channel: { $ref: '#/channels/fulfillmentEventsOrderServiceRetry' }
    summary: order-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [order-service] }
  orderServiceSendsToFulfillmentEventsOrderServiceDlq:
    action: send
    x-producers: [order-service]
    channel: { $ref: '#/channels/fulfillmentEventsOrderServiceDlq' }
    summary: order-service aparta a DLQ lo que agotó los reintentos
  inventoryServiceSendsToFulfillmentEventsInventoryServiceRetry:
    action: send
    x-producers: [inventory-service]
    channel: { $ref: '#/channels/fulfillmentEventsInventoryServiceRetry' }
    summary: inventory-service aparta a reintento lo que falló, sin bloquear la partición
  inventoryServiceReceivesFulfillmentEventsInventoryServiceRetry:
    action: receive
    channel: { $ref: '#/channels/fulfillmentEventsInventoryServiceRetry' }
    summary: inventory-service reintenta cuando vence la espera
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [inventory-service] }
  inventoryServiceSendsToFulfillmentEventsInventoryServiceDlq:
    action: send
    x-producers: [inventory-service]
    channel: { $ref: '#/channels/fulfillmentEventsInventoryServiceDlq' }
    summary: inventory-service aparta a DLQ lo que agotó los reintentos
  # Consola del demo (apps/demo-console, ADR-033): eventos en vivo, solo lectura, grupo propio,
  # desde el último offset. No consume andes.audit.v1 ni los tópicos de reintento y DLQ.
  demoConsoleReceivesCatalogProducts:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/catalogProducts' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesPricingPrices:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/pricingPrices' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesInventoryStockLevels:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/inventoryStockLevels' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesSourcesSync:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/sourcesSync' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesInventoryReservations:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/inventoryReservations' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesOrdersEvents:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/ordersEvents' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesPaymentsCommands:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/paymentsCommands' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesPaymentsEvents:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/paymentsEvents' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesOmsCommands:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/omsCommands' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesOmsEvents:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/omsEvents' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
  demoConsoleReceivesFulfillmentEvents:
    action: receive
    x-observer: true
    channel: { $ref: '#/channels/fulfillmentEvents' }
    summary: demo-console muestra el evento en vivo en control.<zona>, sin efectos
    bindings:
      kafka:
        bindingVersion: '0.5.0'
        groupId: { type: string, enum: [demo-console] }
components:
  messageTraits:
    CloudEventsBinary:
      headers:
        type: object
        required: [ce_specversion, ce_id, ce_source, ce_type, ce_time]
        properties:
          ce_specversion: { type: string, const: '1.0' }
          ce_id: { type: string, description: Identificador único. Base de la deduplicación }
          ce_source: { type: string, description: Servicio productor }
          ce_type: { type: string, description: 'com.andesretail.<dominio>.<hecho>.v1. Cada mensaje fija el suyo con const en sus headers' }
          ce_subject: { type: string, description: Identificador del agregado }
          ce_time: { type: string, format: date-time }
          ce_correlationid: { type: string }
          ce_partnerid: { type: string }
          traceparent: { type: string, description: Contexto de traza W3C }
  messages:
    RawErpChange:
      name: RawErpChange
      title: Cambio crudo del ERP (interno, no estable)
      summary: Solo lo escribe y lo lee el erp-adapter. No es parte del modelo canónico
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.erp.change-captured.v1 }
      payload: { $ref: '#/components/schemas/RawChange' }
    RawWmsChange:
      name: RawWmsChange
      title: Movimiento crudo del WMS (interno, no estable)
      summary: Solo lo escribe y lo lee el wms-adapter. No es parte del modelo canónico
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.wms.change-captured.v1 }
      payload: { $ref: '#/components/schemas/RawChange' }
    ProductChanged:
      name: ProductChanged
      title: Producto creado o modificado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.catalog.product-changed.v1 }
      payload: { $ref: '#/components/schemas/Product' }
    PriceChanged:
      name: PriceChanged
      title: Precio creado o modificado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.pricing.price-changed.v1 }
      payload: { $ref: '#/components/schemas/Price' }
    StockLevelChanged:
      name: StockLevelChanged
      title: Stock físico modificado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.inventory.stock-level-changed.v1 }
      payload: { $ref: '#/components/schemas/StockLevel' }
    SourceSynced:
      name: SourceSynced
      title: Sistema de origen sincronizado hasta una posición
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.sources.synced.v1 }
      payload: { $ref: '#/components/schemas/SourceSync' }
    StockReserved:
      name: StockReserved
      title: Reserva creada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.inventory.stock-reserved.v1 }
      payload: { $ref: '#/components/schemas/Reservation' }
    ReservationConfirmed:
      name: ReservationConfirmed
      title: Reserva confirmada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.inventory.reservation-confirmed.v1 }
      payload: { $ref: '#/components/schemas/Reservation' }
    ReservationReleased:
      name: ReservationReleased
      title: Reserva liberada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.inventory.reservation-released.v1 }
      payload: { $ref: '#/components/schemas/Reservation' }
    ReservationExpired:
      name: ReservationExpired
      title: Reserva vencida
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.inventory.reservation-expired.v1 }
      payload: { $ref: '#/components/schemas/Reservation' }
    OrderCreated:
      name: OrderCreated
      title: Orden aceptada con stock reservado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.order.created.v1 }
      payload: { $ref: '#/components/schemas/OrderEvent' }
    OrderConfirmed:
      name: OrderConfirmed
      title: Orden confirmada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.order.confirmed.v1 }
      payload: { $ref: '#/components/schemas/OrderEvent' }
    OrderCancelled:
      name: OrderCancelled
      title: Orden cancelada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.order.cancelled.v1 }
      payload: { $ref: '#/components/schemas/OrderEvent' }
    OrderDispatched:
      name: OrderDispatched
      title: Orden despachada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.order.dispatched.v1 }
      payload: { $ref: '#/components/schemas/OrderEvent' }
    OrderDelivered:
      name: OrderDelivered
      title: Orden entregada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.order.delivered.v1 }
      payload: { $ref: '#/components/schemas/OrderEvent' }
    PaymentRequested:
      name: PaymentRequested
      title: Solicitud de cobro
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.payment.requested.v1 }
      payload: { $ref: '#/components/schemas/PaymentCommand' }
    PaymentRequestedMasked:
      name: PaymentRequestedMasked
      title: Solicitud de cobro con el token enmascarado (DLQ de payment-service)
      summary: El PaymentRequested original con payment.token reemplazado por --data_redacted-- (ADR-029)
      description: Mismo sobre que PaymentRequested, con ce_type com.andesretail.payment.requested.v1 sin cambios. No fija ce_type como constante para no confundirse con PaymentRequested en los índices por tipo; lo distingue la cabecera andes_payload_redacted
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        required: [andes_payload_redacted]
        properties:
          andes_payload_redacted:
            type: string
            description: Campos del payload enmascarados al pasar a la DLQ, separados por coma (ADR-029); `*` si el payload no era JSON y se enmascaró entero
      payload: { $ref: '#/components/schemas/PaymentCommandMasked' }
    RefundRequested:
      name: RefundRequested
      title: Solicitud de reembolso
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.payment.refund-requested.v1 }
      payload: { $ref: '#/components/schemas/RefundCommand' }
    PaymentApproved:
      name: PaymentApproved
      title: Pago aprobado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.payment.approved.v1 }
      payload: { $ref: '#/components/schemas/PaymentEvent' }
    PaymentRejected:
      name: PaymentRejected
      title: Pago rechazado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.payment.rejected.v1 }
      payload: { $ref: '#/components/schemas/PaymentEvent' }
    PaymentRefunded:
      name: PaymentRefunded
      title: Pago reembolsado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.payment.refunded.v1 }
      payload: { $ref: '#/components/schemas/PaymentEvent' }
    CreateOmsOrder:
      name: CreateOmsOrder
      title: Registrar orden confirmada en el OMS
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.oms.create-order.v1 }
      payload: { $ref: '#/components/schemas/OmsCommand' }
    CancelOmsOrder:
      name: CancelOmsOrder
      title: Cancelar orden en el OMS
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.oms.cancel-order.v1 }
      payload: { $ref: '#/components/schemas/OmsCommand' }
    OmsOrderCreated:
      name: OmsOrderCreated
      title: Orden registrada en el OMS
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.oms.order-created.v1 }
      payload: { $ref: '#/components/schemas/OmsEvent' }
    OmsOrderFailed:
      name: OmsOrderFailed
      title: El OMS rechazó la orden de forma definitiva
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.oms.order-failed.v1 }
      payload: { $ref: '#/components/schemas/OmsEvent' }
    DispatchStarted:
      name: DispatchStarted
      title: Despacho iniciado
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.fulfillment.dispatch-started.v1 }
      payload: { $ref: '#/components/schemas/FulfillmentEvent' }
    DeliveryConfirmed:
      name: DeliveryConfirmed
      title: Entrega confirmada
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.fulfillment.delivered.v1 }
      payload: { $ref: '#/components/schemas/FulfillmentEvent' }
    AuditRecorded:
      name: AuditRecorded
      title: Registro de auditoría
      traits: [{ $ref: '#/components/messageTraits/CloudEventsBinary' }]
      headers:
        type: object
        properties:
          ce_type: { type: string, const: com.andesretail.audit.recorded.v1 }
      payload: { $ref: '#/components/schemas/AuditRecord' }
  schemas:
    MaskedValue:
      type: string
      const: '--data_redacted--'
      description: Valor fijo de un dato sensible enmascarado al pasar a una DLQ (ADR-029). El campo enmascarado queda listado en la cabecera andes_payload_redacted
    Money:
      type: string
      pattern: '^\d+\.\d{2}$'
    RawChange:
      type: object
      description: >-
        Cambio del legado tal como lo leyó el adaptador, sin traducir. Interno
        del adaptador y no estable; su forma sigue al legado y puede cambiar sin
        aviso ni versión nueva del tópico. Ningún servicio de dominio lo
        consume; lo que sale de la capa es el evento canónico. Solo trae datos
        de producto, precio y stock, nunca datos personales
      required: [system, position, capturedAt, record]
      properties:
        system: { type: string, enum: [erp, wms] }
        position:
          type: string
          description: Posición del cambio en el feed del legado (lsn del ERP, seq del WMS), para retomar la lectura y deduplicar
        capturedAt:
          type: string
          format: date-time
          description: Momento en que el adaptador leyó el cambio
        record:
          type: object
          additionalProperties: true
          description: Fila o movimiento con los nombres de campo del legado
    Product:
      type: object
      required: [sku, status, channels, updatedAt]
      properties:
        sku: { type: string }
        name: { type: string }
        status: { type: string, enum: [ACTIVE, INACTIVE] }
        channels:
          type: array
          description: Canales en los que se puede vender (surtido). Un SKU fuera de esta lista sale en notFound de stock y de precio para ese canal (ADR-019)
          items: { type: string }
        updatedAt: { type: string, format: date-time }
    Price:
      type: object
      required: [sku, channel, priceList, currency, listPrice, salePrice, priceVersion, validFrom, updatedAt]
      properties:
        sku: { type: string }
        channel: { type: string }
        priceList: { type: string }
        currency: { type: string, pattern: '^[A-Z]{3}$', description: Código ISO 4217 }
        listPrice: { $ref: '#/components/schemas/Money' }
        salePrice: { $ref: '#/components/schemas/Money' }
        taxIncluded: { type: boolean, default: false }
        priceVersion: { type: string }
        validFrom: { type: string, format: date-time }
        validTo: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        correction:
          type: boolean
          default: false
          description: >-
            true si el cambio corrige un precio mal cargado. Las versiones
            anteriores de la misma llave (sku|channel|priceList) dejan de
            aceptarse de inmediato al validar una orden, aunque estén dentro de
            la ventana de tolerancia del canal. Lo marca el erp-adapter según el
            motivo del cambio en el ERP. Ausente equivale a false (ADR-015)
        schedule:
          type: array
          maxItems: 10
          description: >-
            Calendario del alcance sku|channel|priceList: todas sus ventanas no
            vencidas, incluida la vigente al publicar (ADR-020). Regla de
            selección: entre las ventanas con validFrom <= ahora y sin validTo o
            con ahora < validTo, gana la de validFrom más reciente; si ninguna
            está vigente, no hay precio. Hora UTC. Los campos de primer nivel son
            la ventana vigente al publicar; un consumidor que no lee schedule se
            comporta como antes. Ausente equivale a una sola ventana, la de
            primer nivel. Si al publicar ninguna ventana rige, el primer nivel es
            la próxima futura; si todas vencieron, schedule va vacío y el primer
            nivel es la más reciente, con su validTo ya pasado. Un schedule
            presente, aunque esté vacío, manda sobre el primer nivel
          items: { $ref: '#/components/schemas/PriceWindow' }
    PriceWindow:
      type: object
      required: [currency, listPrice, salePrice, priceVersion, validFrom]
      properties:
        currency: { type: string, pattern: '^[A-Z]{3}$', description: Código ISO 4217 }
        listPrice: { $ref: '#/components/schemas/Money' }
        salePrice: { $ref: '#/components/schemas/Money' }
        taxIncluded: { type: boolean, default: false }
        priceVersion: { type: string }
        validFrom: { type: string, format: date-time }
        validTo:
          type: string
          format: date-time
          description: Fin de la ventana, excluido. Ausente es sin fin
    SourceSync:
      type: object
      required: [system, position, syncedAt]
      properties:
        system: { type: string, enum: [erp, wms], description: Sistema de origen. Es la clave del mensaje }
        position:
          type: string
          description: Última posición del feed cuyos cambios ya están publicados y confirmados (lsn del ERP o seq del WMS), en texto
        syncedAt:
          type: string
          format: date-time
          description: Momento UTC del último sondeo exitoso al legado. Si el legado no responde o el circuito está abierto, no avanza
    StockLevel:
      type: object
      required: [sku, locationId, onHand, wmsAsOf]
      properties:
        sku: { type: string }
        locationId: { type: string }
        onHand: { type: integer, minimum: 0 }
        wmsAsOf:
          type: string
          format: date-time
          description: Momento del dato en el WMS. Un evento más viejo que el dato guardado se descarta
        snapshot:
          type: boolean
          description: true si proviene de la conciliación completa
    Reservation:
      type: object
      required: [reservationId, orderId, partnerId, status, lines]
      properties:
        reservationId: { type: string }
        orderId: { type: string }
        partnerId: { type: string }
        status: { type: string, enum: [HELD, CONFIRMED, RELEASED, EXPIRED, CONSUMED] }
        reason: { type: string }
        expiresAt: { type: string, format: date-time }
        lines:
          type: array
          items:
            type: object
            required: [sku, locationId, quantity]
            properties:
              sku: { type: string }
              locationId: { type: string }
              quantity: { type: integer, minimum: 1 }
    OrderEvent:
      type: object
      description: Sin datos personales. Quien necesite la dirección la pide a order-service con el orderId
      required: [orderId, externalOrderId, partnerId, channel, status, occurredAt]
      properties:
        orderId: { type: string }
        externalOrderId: { type: string }
        partnerId: { type: string }
        channel: { type: string }
        customerId: { type: string }
        status: { type: string, enum: [PENDING_PAYMENT, CONFIRMED, CANCELLED, DISPATCHED, DELIVERED] }
        reason: { type: string }
        total:
          type: object
          properties:
            amount: { $ref: '#/components/schemas/Money' }
            currency: { type: string }
        lines:
          type: array
          items:
            type: object
            required: [sku, quantity, unitPrice]
            properties:
              sku: { type: string }
              locationId: { type: string }
              quantity: { type: integer }
              unitPrice: { $ref: '#/components/schemas/Money' }
        occurredAt: { type: string, format: date-time }
    PaymentCommand:
      type: object
      required: [orderId, partnerId, amount, currency]
      if: { properties: { mode: { const: CAPTURE } }, required: [mode] }
      then: { required: [token] }
      properties:
        orderId:
          type: string
          description: También es la llave de idempotencia hacia la pasarela
        partnerId: { type: string }
        mode: { type: string, enum: [CAPTURE, PREPAID] }
        method: { type: string }
        token:
          type: string
          description: Token de la pasarela. Nunca datos de tarjeta. Dato sensible; en la DLQ llega enmascarado (MaskedValue, ADR-029)
        amount: { $ref: '#/components/schemas/Money' }
        currency: { type: string }
        expiresAt:
          type: string
          format: date-time
          description: Límite para obtener resultado; coincide con el vencimiento de la reserva
    PaymentCommandMasked:
      description: PaymentCommand tal como queda en la DLQ de payment-service, con el token enmascarado (ADR-029)
      allOf:
        - { $ref: '#/components/schemas/PaymentCommand' }
        - type: object
          properties:
            token: { $ref: '#/components/schemas/MaskedValue' }
    RefundCommand:
      type: object
      description: Reembolso de un cobro ya aprobado (compensación de la saga). Sin token ni datos de tarjeta
      required: [orderId, partnerId, amount, currency]
      properties:
        orderId: { type: string, description: Orden del cobro que se reembolsa }
        partnerId: { type: string }
        mode:
          type: string
          enum: [CAPTURE, PREPAID]
          description: El del cobro original. Un PREPAID se registra sin llamar a la pasarela
        amount: { $ref: '#/components/schemas/Money' }
        currency: { type: string }
        reason: { type: string, description: Motivo de la compensación, por ejemplo LATE_APPROVAL u ORDER_CANCELLED }
    PaymentEvent:
      type: object
      required: [orderId, partnerId, status, amount, currency, occurredAt]
      properties:
        orderId: { type: string }
        partnerId: { type: string }
        status: { type: string, enum: [APPROVED, REJECTED, REFUNDED] }
        gatewayReference: { type: string }
        reasonCode:
          type: string
          enum: [CARD_DECLINED, DECLINED, AUTHENTICATION_REQUIRED, PAYMENT_TIMEOUT, PAYMENT_EXPIRED, INVALID_PAYMENT_REQUEST, LATE_APPROVAL]
          description: >-
            Causa de un REJECTED, o de un REFUNDED automático (LATE_APPROVAL: la
            pasarela aprobó después de que la capa rechazó por tiempo). Ausente en
            un APPROVED. order-service lo traduce a statusReason PAYMENT_REJECTED
        amount: { $ref: '#/components/schemas/Money' }
        currency: { type: string }
        occurredAt: { type: string, format: date-time }
    OmsCommand:
      type: object
      description: >-
        Sin datos personales. CreateOmsOrder lleva lo que el OMS necesita de la
        orden, salvo destinatario y dirección (ADR-025), para que oms-adapter no
        guarde estado de negocio. CancelOmsOrder solo necesita orderId y reason
      required: [orderId]
      properties:
        orderId: { type: string }
        externalOrderId: { type: string }
        partnerId: { type: string }
        channel: { type: string }
        reason: { type: string }
        total:
          type: object
          properties:
            amount: { $ref: '#/components/schemas/Money' }
            currency: { type: string }
        lines:
          type: array
          items:
            type: object
            required: [sku, quantity, unitPrice]
            properties:
              sku: { type: string }
              locationId: { type: string }
              quantity: { type: integer }
              unitPrice: { $ref: '#/components/schemas/Money' }
    OmsEvent:
      type: object
      required: [orderId, occurredAt]
      properties:
        orderId: { type: string }
        omsOrderId: { type: string }
        errorCode: { type: string }
        occurredAt: { type: string, format: date-time }
    FulfillmentEvent:
      type: object
      required: [orderId, occurredAt]
      properties:
        orderId: { type: string }
        omsOrderId: { type: string }
        locationId: { type: string }
        carrier: { type: string }
        trackingCode: { type: string }
        lines:
          type: array
          items:
            type: object
            properties:
              sku: { type: string }
              quantity: { type: integer }
        occurredAt: { type: string, format: date-time }
    AuditRecord:
      type: object
      required: [correlationId, actor, action, resource, result, occurredAt]
      properties:
        correlationId: { type: string }
        actor:
          type: object
          required: [type, id]
          properties:
            type: { type: string, enum: [PARTNER, OPERATOR, SERVICE] }
            id: { type: string }
        action: { type: string }
        resource: { type: string }
        result: { type: string, enum: [SUCCESS, DENIED, FAILED] }
        origin: { type: string }
        occurredAt: { type: string, format: date-time }
