{"openapi":"3.1.0","info":{"title":"Delivery Adapter API","version":"1.0.0","description":"Adapter de ultima milla (Bringoz / Nexus) para el Core Service Bus de Rotoplas Bebbia. Recibe tareas de Cloud Tasks, invoca al proveedor de entregas activo y publica el estado de las entregas en Pub/Sub. NUNCA se expone directamente a internet.\n\nEste documento se mantiene a mano en `src/docs/public/openapi.json` (el adapter no usa @nestjs/swagger). Si un DTO cambia, actualizar aqui."},"servers":[{"url":"/","description":"Este servicio"},{"url":"http://localhost:8080","description":"Desarrollo local"},{"url":"http://localhost:8083","description":"docker-compose.integration.yml"}],"tags":[{"name":"delivery","description":"Operaciones de entrega contra el proveedor activo (DELIVERY_PROVIDER)"},{"name":"webhooks","description":"Callbacks del proveedor de entregas"},{"name":"health","description":"Liveness / readiness"}],"paths":{"/delivery/events":{"post":{"tags":["delivery"],"operationId":"DeliveryController_events","summary":"Push de Pub/Sub — solicitud de creacion de orden","description":"Endpoint de una push subscription de Pub/Sub (OIDC con la SA `sa-csb-api-<env>`, que tiene `run.invoker` sobre este servicio). El CSB publica un EventEnvelope CloudEvents v1.0 en su topic; Pub/Sub lo entrega aqui con `message.data` en base64.\n\nEl adapter decodifica y valida el envelope; si `type` es uno de `delivery.order.requested.v1` | `order.placed.v1` lo procesa igual que `POST /delivery/create-order`, sintetizando `taskMetadata` (`taskName` = `message.messageId`, `retryCount` = 0, `queueName` = `subscription`).\n\nRespuestas: 204 al procesar (Pub/Sub hace ack); 400 si el payload, el envelope o el `type` no aplican — Pub/Sub NO reintenta un 4xx, por lo que un mensaje irrecuperable no se queda en bucle; 500 si falla el proveedor, y ahi si Pub/Sub reintenta hasta el ack deadline.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PubSubPushDto"}}}},"responses":{"204":{"description":"Evento procesado (sin cuerpo). Pub/Sub hace ack."},"400":{"description":"message.data no es JSON base64, el envelope es invalido, o el `type` no esta soportado. Pub/Sub no reintenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Error del proveedor o interno. Pub/Sub reintenta.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/delivery/create-order":{"post":{"tags":["delivery"],"operationId":"DeliveryController_createOrder","summary":"Crear orden de entrega","description":"Invocado por Cloud Tasks desde el CSB. Recibe un envelope cuyo `data` es el body Bringoz ya mapeado (externalId `{IDFluent}-{Kindflag}`, fulfillmentLines, itemLines, metaData, tags) y hace `PUT /v2/oms/orders-v2?siteId=` en Bringoz (retry x3 con backoff exponencial ante 5xx). `accountId` se completa desde BRINGOZ_ACCOUNT_ID si no viene. 2xx → `accepted`; 4xx → `rejected` (HTTP 200, con el body de Bringoz en `raw`); 5xx/red agotados → 500.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryPayloadDto"}}}},"responses":{"200":{"description":"Orden aceptada por el adapter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderResponse"}}}},"400":{"description":"Payload invalido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/delivery/track/{orderId}":{"get":{"tags":["delivery"],"operationId":"DeliveryController_track","summary":"Consultar estado de una orden","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string"},"description":"Identificador de la orden en el proveedor"}],"responses":{"200":{"description":"Estado actual","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"description":"No cubierto por la doc \"Conexion Bringoz QAS\" de Bebbia (el cierre llega por webhook). Devuelve `pending` sin llamar a Bringoz hasta confirmar el endpoint."}},"/delivery/cancel/{orderId}":{"post":{"tags":["delivery"],"operationId":"DeliveryController_cancel","summary":"Cancelar una orden de entrega","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelOrderBody"}}}},"responses":{"200":{"description":"Orden cancelada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"description":"`POST /v2/oms/orders-v2/cancel/{externalId}?siteId=` en Bringoz (compensacion, retry x1). `orderId` es el externalId `{IDFluent}-{Kindflag}`. `reason` se envia como `reason` y `description` del body de Bringoz (Bebbia usa `Cancelacion-Flow_failed`)."}},"/delivery/{orderId}/lines/{lineId}/depots":{"post":{"tags":["delivery"],"operationId":"DeliveryController_depots","summary":"Depots candidatos del area (Bringoz: POST .../options/locations)","description":"Estrategia `LOCATION_DEPOT_IN_OPERATIONAL_UNIT` con `taskPurpose` opcional. Se filtran como Bebbia: `areaIds` no vacio y `externalId` de exactamente 8 caracteres (locationRef de Fluent). Retry x3.","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string"},"description":"externalId de la orden en Bringoz: `{IDFluent}-{Kindflag}`"},{"name":"lineId","in":"path","required":true,"schema":{"type":"string"},"description":"Line id en Bringoz: el IDFluent solo"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepotsRequestDto"}}}},"responses":{"200":{"description":"Depots filtrados","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepotsResponse"}}}},"400":{"description":"Body invalido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"El proveedor activo no implementa agendado (Nexus)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/delivery/{orderId}/lines/{lineId}/time-slots":{"post":{"tags":["delivery"],"operationId":"DeliveryController_timeSlots","summary":"Cotizar horarios (Bringoz: POST .../options/summary, DynamicTimeSlot)","description":"Estrategia `LOCATION_DEPOT_LIST` con hasta 5 depots. `notBefore`/`notAfter` en epoch ms; `secToWait` 2 s por defecto (Bebbia usa 20 para auronix). Sin retry. Si `quote.pending` es true, consultar `GET .../time-slots/{quoteId}`.","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string"},"description":"externalId de la orden en Bringoz: `{IDFluent}-{Kindflag}`"},{"name":"lineId","in":"path","required":true,"schema":{"type":"string"},"description":"Line id en Bringoz: el IDFluent solo"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeSlotsRequestDto"}}}},"responses":{"200":{"description":"Quote abierto o resuelto","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"400":{"description":"Body invalido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"El proveedor activo no implementa agendado (Nexus)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/delivery/{orderId}/lines/{lineId}/time-slots/{quoteId}":{"get":{"tags":["delivery"],"operationId":"DeliveryController_timeSlotQuote","summary":"Polling del quote asincrono (Bringoz: GET .../options/summary/{quoteId})","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string"},"description":"externalId de la orden en Bringoz: `{IDFluent}-{Kindflag}`"},{"name":"lineId","in":"path","required":true,"schema":{"type":"string"},"description":"Line id en Bringoz: el IDFluent solo"},{"name":"quoteId","in":"path","required":true,"schema":{"type":"string"},"description":"`quote.quoteId` devuelto por la cotizacion"}],"responses":{"200":{"description":"Estado actual del quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"El proveedor activo no implementa agendado (Nexus)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/delivery/{orderId}/lines/{lineId}/dispatch/{optionId}":{"post":{"tags":["delivery"],"operationId":"DeliveryController_dispatch","summary":"Asignar el slot elegido (Bringoz: POST /v2/oms/orders/.../options/dispatch/{optionId}/)","description":"Sin body. Retry x3. Un 404 de Bringoz (HTTP o embebido en un 200 con `\"status\":404`) se devuelve como `rejected` para que el CSB vuelva a cotizar.","parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string"},"description":"externalId de la orden en Bringoz: `{IDFluent}-{Kindflag}`"},{"name":"lineId","in":"path","required":true,"schema":{"type":"string"},"description":"Line id en Bringoz: el IDFluent solo"},{"name":"optionId","in":"path","required":true,"schema":{"type":"string"},"description":"`optionId` de la cotizacion"}],"responses":{"200":{"description":"Slot asignado o rechazado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DispatchResponse"}}}},"500":{"description":"Error del proveedor o interno","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"El proveedor activo no implementa agendado (Nexus)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/bringoz":{"post":{"tags":["webhooks"],"operationId":"WebhooksController_bringozCallback","summary":"Callback de estado de Bringoz","description":"Bringoz notifica cambios de estado de una entrega. El evento se mapea a `DeliveryResult` y se publica en el topic de resultados (`PUBSUB_RESULT_TOPIC`, default `delivery.completed.v1`) con atributos `eventId`, `provider`, `status`, `webhookEvent`. No hay validacion de firma (pendiente de la spec de Bringoz).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEventDto"}}}},"responses":{"200":{"description":"Evento aceptado y publicado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookAckResponse"}}}},"400":{"description":"Evento invalido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Fallo al publicar en Pub/Sub","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/health":{"get":{"tags":["health"],"operationId":"HealthController_check","summary":"Liveness probe","responses":{"200":{"description":"Servicio vivo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/health/ready":{"get":{"tags":["health"],"operationId":"HealthController_ready","summary":"Readiness probe","description":"`status` es `ok` cuando el proveedor activo tiene `host` configurado; `not_configured` en caso contrario (siempre HTTP 200).","responses":{"200":{"description":"Estado de configuracion del proveedor","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyResponse"}}}}}}}},"components":{"schemas":{"EventEnvelopeDto":{"type":"object","description":"Envelope CloudEvents v1.0 que circula por los topics del CSB (mismo `envelope` que usan los adapters ERP/CRM). Viaja serializado como JSON en `message.data` (base64).","required":["id","type","source","specversion","data"],"properties":{"id":{"type":"string","description":"UUID del evento; se registra como `event_id` y sirve para deduplicar.","examples":["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]},"type":{"type":"string","description":"Tipo del evento. `/delivery/events` solo acepta `delivery.order.requested.v1` y `order.placed.v1`; cualquier otro -> 400.","examples":["delivery.order.requested.v1"]},"source":{"type":"string","description":"Origen del evento.","examples":["//bebbia/order-service"]},"specversion":{"type":"string","description":"Version de CloudEvents.","examples":["1.0"]},"time":{"type":"string","format":"date-time","description":"Fecha ISO-8601 de emision (opcional)."},"entityid":{"type":"string","description":"ID de la entidad de negocio (ordering key). Se mapea a `envelope.entityId` del flujo de create-order.","examples":["ORD-000123"]},"datacontenttype":{"type":"string","examples":["application/json"]},"data":{"type":"object","additionalProperties":true,"description":"Payload de negocio: el body de Bringoz ya mapeado por el CSB (mismo contenido que `envelope.data` de DeliveryPayloadDto)."}}},"PubSubPushMessageDto":{"type":"object","description":"Mensaje que Pub/Sub entrega en una push subscription. https://cloud.google.com/pubsub/docs/push","required":["data","messageId"],"properties":{"data":{"type":"string","format":"byte","description":"EventEnvelope (CloudEvents v1.0) serializado como JSON y codificado en base64."},"attributes":{"type":"object","additionalProperties":{"type":"string"},"description":"Atributos del mensaje (no se usan hoy para enrutar)."},"messageId":{"type":"string","description":"ID del mensaje asignado por Pub/Sub; se usa como `taskMetadata.taskName`.","examples":["11209384756102"]},"publishTime":{"type":"string","format":"date-time"}}},"PubSubPushDto":{"type":"object","description":"Body de `POST /delivery/events`.","required":["message","subscription"],"properties":{"message":{"$ref":"#/components/schemas/PubSubPushMessageDto"},"subscription":{"type":"string","description":"Nombre completo de la suscripcion; se registra como `taskMetadata.queueName`.","examples":["projects/rtp-transversal-dev/subscriptions/csb-delivery-orders-sub"]}}},"DeliveryEnvelopeDto":{"type":"object","required":["id","data"],"properties":{"id":{"type":"string","description":"UUID del evento de dominio (usado como event_id en logs)","examples":["a1b2c3d4-e5f6-7890-abcd-ef1234567890"]},"entityId":{"type":"string","description":"Identificador de la entidad de negocio (ordering key)","examples":["ORD-000123"]},"correlationId":{"type":"string","description":"Correlacion end-to-end; si no viene, el middleware genera X-Correlation-ID"},"data":{"type":"object","additionalProperties":true,"description":"Body de Bringoz ya mapeado por el CSB (ver doc \"Conexion Bringoz QAS\"). `accountId` opcional (se completa desde configuracion).","examples":[{"externalId":"FC-1001-Installation","fulfillmentLines":[{"id":"FC-1001","destinationTask":{"notes":"","contact":{"externalId":"SF_customer_id","name":"Ana Lopez","phoneNo":"5512345678"},"location":{"address":{"location":{"latitude":"19.4326","longitude":"-99.1332"},"city":"CDMX","country":"MX","street":"Av. Insurgentes Sur 1234","zipCode":"03100","state":"CDMX"}},"purpose":"Dropoff"},"lineItems":[{"id":"300432"}],"programName":"INSTALACION","type":"Delivery"}],"itemLines":[{"id":"300432","sku":"300432","description":"Purificador","count":1,"type":"BasePack","weight":0,"dimensions":{"height":0,"width":0,"length":0}}],"metaData":[{"key":"OrderNumber_CT_FC","value":"1001"},{"key":"ServiceType","value":"Instalacion-NuevoActivo"},{"key":"SKU","value":"300432"}],"tags":["Entrega","3400127382"]}]}}},"TaskMetadataDto":{"type":"object","required":["queueName"],"properties":{"queueName":{"type":"string","examples":["csb-delivery-adapter-queue"]},"retryCount":{"type":"number","minimum":0,"examples":[0]}}},"DeliveryPayloadDto":{"type":"object","required":["envelope"],"properties":{"envelope":{"$ref":"#/components/schemas/DeliveryEnvelopeDto"},"taskMetadata":{"$ref":"#/components/schemas/TaskMetadataDto"}}},"CancelOrderBody":{"type":"object","properties":{"reason":{"type":"string","description":"Motivo de cancelacion (cadena vacia si se omite)","examples":["Cancelacion-Flow_failed"]}}},"DeliveryResult":{"type":"object","required":["orderId","status","providerRef"],"properties":{"orderId":{"type":"string"},"status":{"type":"string","enum":["accepted","rejected","pending","delivered","cancelled"]},"providerRef":{"type":"string","description":"Referencia de la orden en el proveedor (externalId en Bringoz)"},"trackingUrl":{"type":"string","format":"uri"},"raw":{"type":"object","additionalProperties":true,"description":"Payload crudo del proveedor o del webhook"}}},"CreateOrderResponse":{"type":"object","required":["status","provider","result"],"properties":{"status":{"type":"string","const":"ok"},"provider":{"type":"string","enum":["bringoz","nexus"],"description":"Proveedor activo (DELIVERY_PROVIDER)"},"result":{"$ref":"#/components/schemas/DeliveryResult"}}},"ResultResponse":{"type":"object","required":["status","result"],"properties":{"status":{"type":"string","const":"ok"},"result":{"$ref":"#/components/schemas/DeliveryResult"}}},"WebhookEventDto":{"type":"object","required":["eventType","orderId"],"properties":{"eventType":{"type":"string","description":"order_accepted → accepted · order_rejected → rejected · order_delivered → delivered · order_cancelled → cancelled · order_in_transit → pending · otro → pending","examples":["order_delivered"]},"orderId":{"type":"string","examples":["ORD-000123"]},"status":{"type":"string","description":"Estado textual del proveedor (solo logging)"},"trackingUrl":{"type":"string","format":"uri"},"details":{"type":"object","additionalProperties":true},"timestamp":{"type":"string","format":"date-time"}}},"WebhookAckResponse":{"type":"object","required":["status","acknowledged"],"properties":{"status":{"type":"string","const":"ok"},"acknowledged":{"type":"boolean","const":true}}},"HealthResponse":{"type":"object","required":["status","timestamp","service","version","provider"],"properties":{"status":{"type":"string","const":"ok"},"timestamp":{"type":"string","format":"date-time"},"service":{"type":"string","const":"csb-delivery-adapter"},"version":{"type":"string","examples":["1.0.0"]},"provider":{"type":"string","enum":["bringoz","nexus"]}}},"ReadyResponse":{"type":"object","required":["status","timestamp","provider"],"properties":{"status":{"type":"string","enum":["ok","not_configured"]},"timestamp":{"type":"string","format":"date-time"},"provider":{"type":"string","enum":["bringoz","nexus"]}}},"ErrorResponse":{"type":"object","description":"Formato del GlobalExceptionFilter","required":["success","statusCode","error","timestamp"],"properties":{"success":{"type":"boolean","const":false},"statusCode":{"type":"integer","examples":[400]},"error":{"type":"string","examples":["Bad Request Exception"]},"timestamp":{"type":"string","format":"date-time"}}},"DepotsRequestDto":{"type":"object","properties":{"taskPurpose":{"type":"string","enum":["Dropoff","Operation","Pickup"],"description":"Opcional. Bebbia: Dropoff para Substitution/Cancellation/RecuperacionPorInactividad, Pickup para ChangeTech/ReplaceTech, omitido en el resto."},"zoneId":{"type":"string","examples":["America/Mexico_City"]}}},"TimeSlotsRequestDto":{"type":"object","required":["depotIdList","notBefore","notAfter"],"properties":{"depotIdList":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":5,"description":"Ids de depots (`depots[].id`), maximo 5"},"taskPurpose":{"type":"string","enum":["Dropoff","Operation","Pickup"],"description":"Solo ChangeTech/ReplaceTech (Pickup)"},"notBefore":{"type":"number","description":"epoch ms","examples":[1700000000000]},"notAfter":{"type":"number","description":"epoch ms","examples":[1700604800000]},"zoneId":{"type":"string","examples":["America/Mexico_City"]},"secToWait":{"type":"integer","minimum":1,"description":"Segundos que Bringoz espera antes de responder (2; 20 para auronix)","examples":[2]}}},"DepotOption":{"type":"object","required":["id","externalId"],"properties":{"id":{"type":"string"},"externalId":{"type":"string","description":"locationRef de Fluent (8 chars)"},"zoneId":{"type":"string"},"raw":{"type":"object","additionalProperties":true}}},"DepotsResponse":{"type":"object","required":["status","provider","depots"],"properties":{"status":{"type":"string","const":"ok"},"provider":{"type":"string","enum":["bringoz","nexus"]},"depots":{"type":"array","items":{"$ref":"#/components/schemas/DepotOption"}}}},"TimeSlotOption":{"type":"object","required":["optionId","notBefore","notAfter"],"properties":{"optionId":{"type":"string"},"notBefore":{"type":"number","description":"epoch ms"},"notAfter":{"type":"number","description":"epoch ms"},"pickupNotBefore":{"type":"number","description":"epoch ms (Cancellation, Substitution, RecuperacionPorInactividad)"},"pickupNotAfter":{"type":"number"},"depotId":{"type":"string"},"depotExternalId":{"type":"string"},"zoneId":{"type":"string"}}},"TimeSlotQuote":{"type":"object","required":["quoteId","status","pending","options"],"properties":{"quoteId":{"type":"string"},"status":{"type":"string","description":"Estado reportado por Bringoz"},"pending":{"type":"boolean","description":"true si el quote sigue PENDING y hay que volver a consultarlo"},"options":{"type":"array","items":{"$ref":"#/components/schemas/TimeSlotOption"}},"raw":{"type":"object","additionalProperties":true}}},"QuoteResponse":{"type":"object","required":["status","provider","quote"],"properties":{"status":{"type":"string","const":"ok"},"provider":{"type":"string","enum":["bringoz","nexus"]},"quote":{"$ref":"#/components/schemas/TimeSlotQuote"}}},"DispatchResult":{"type":"object","required":["orderId","optionId","status","providerRef"],"properties":{"orderId":{"type":"string"},"optionId":{"type":"string"},"status":{"type":"string","enum":["assigned","rejected"]},"providerRef":{"type":"string","description":"orderExternalId devuelto por Bringoz"},"raw":{"type":"object","additionalProperties":true}}},"DispatchResponse":{"type":"object","required":["status","provider","result"],"properties":{"status":{"type":"string","const":"ok"},"provider":{"type":"string","enum":["bringoz","nexus"]},"result":{"$ref":"#/components/schemas/DispatchResult"}}}}}}