Core Service Bus
Capa de integración event-driven de Rotoplas Bebbia sobre GCP. Elimina el acoplamiento punto a punto entre microservicios y sistemas externos combinando coreografía (Pub/Sub) y orquestación (Cloud Workflows + Cloud Tasks), con un control plane (csb-api), un editor visual de flujos (csb-web) y adapters independientes por sistema destino.
Resumen
Qué resuelve
Un productor publica un hecho de negocio una sola vez (o llama a un WebHook firmado) y el bus decide qué sistemas deben enterarse y en qué orden: reintentos, compensaciones, DLQ, auditoría y trazabilidad viven en el bus, no en cada aplicación. Los sistemas destino (SAP, HubSpot, Bringoz, Fluent, CommerceTools, Data Mesh, Portal de Inventario) se alcanzan por adapters o por definiciones de API registradas.
Dos patrones, una plataforma
Coreografía: topics Pub/Sub con schema; consumers independientes (audit, analytics, notification) reaccionan sin conocerse. Orquestación: Cloud Workflows (sagas con compensación) y Cloud Tasks para el trabajo diferido hacia adapters. El editor visual dibuja ambos y deployFlow crea los recursos GCP reales.
Identidad
- Proyecto GCP
rtp-transversal-dev·us-central1- Dominio
csb-dev.rotoplas.com(LB HTTPS + Cloud Armor)- Repo
infraestructura6863429/arquitectura/rtp-csb· rama activadev- Arquitectura
docs/CSB-Arquitectura-Solucion-v1.1.0.pdf(mayo 2026) · ADR-001 SAP BTP
Equipo y estado
- Stakeholder
- Hugo Quintero (ArchOps)
- Desarrollo
- Héctor Cruz, Carlos Martínez
- Épica
- AE-36 cerrada 20/20 en código; AE-137 con ACs humanas (firma SAP + siembra de secretos)
- Pruebas
- 694 en este repo (api 542 · web 119 · audit 13 · analytics 8 · notification 12)
Arquitectura
Reglas que fija el diseño: solo el CSB le pega a SAP y a HubSpot (ADR-001); todo recurso GCP nace en Terraform o en deployFlow con etiquetas managed_by; los secretos viven en Secret Manager con nombre csb-<env>-<name>; ningún servicio se expone directo a internet, solo por el balanceador con Cloud Armor.
Componentes y URLs
| Componente | Qué hace | Dónde vive | URL (dev) |
|---|---|---|---|
csb-api | Control plane NestJS 10: flows, topics, schemas, adapters, connectors, api-registry, secrets, provisioning (deploy/preview/execute/redeploy), monitoring, webhooks, import/export, bindings por ambiente, memoria compartida, auth (JWT + Google OAuth + API key + roles) | apps/api · Cloud Run csb-api (SA sa-csb-api-dev) | https://csb-dev.rotoplas.com/api · Swagger en /api/docs/api · este portal en /api/docs |
csb-web | Next.js 16 + React Flow: editor visual (saga builder), registry, monitoring, arquitectura y contratos, login Google | apps/web · Cloud Run csb-web | https://csb-dev.rotoplas.com → /flows, /registry/*, /monitoring/*, /architecture, /contracts, /settings/secrets |
csb-audit-consumer | Pull listener de inventory.stock.updated.v1.audit.sub → tabla audit_log en el mismo Cloud SQL Postgres | apps/audit-consumer · Cloud Run (min 1) | interno |
csb-analytics-consumer | Pull → BigQuery csb_analytics.csb_events (particionada por día, insertId = event_id) | apps/analytics-consumer · Cloud Run | interno |
csb-notification-consumer | Pull → dispara send-notification-workflow cuando quantity_after < reorder_point | apps/notification-consumer · Cloud Run | interno |
| Adapters | ERP (SAP CPI + OData), CRM (HubSpot), Delivery (Bringoz/Nexus). Repos y Terraform propios; el CSB lee sus outputs por remote state | rtp-erp-adapter, rtp-crm-adapter, rtp-delivery-adapter | /adapters/erp/docs · /adapters/crm/docs · /adapters/delivery/docs |
packages/shared | Contratos compartidos: pubsub.contracts.ts, cloud-tasks.contracts.ts, workflows.contracts.ts, csb-api.contracts.ts | packages/shared/src/contracts | — |
Endpoints principales de csb-api
| Área | Rutas | Auth |
|---|---|---|
| Health | GET /health (incluye env y version) · /health/ready | pública |
| Auth | POST /auth/login · GET /auth/google, /auth/google/callback | pública |
| Registry | /topics, /schemas (+ /:id/activate), /api-registry (+ test-connection), /connectors, /adapters (+ /:id/probe), /secrets | API key / JWT + roles |
| Flujos | /flows, /flows/:id/graph, /flows/:id/publish, /flows/:id/versions, /flows/:id/preview, /flows/:id/validate, /flows/presence | API key / JWT + roles |
| Provisioning | POST /provisioning/flows/:id/deploy · /execute · /redeploy/:deploymentId · GET /preview, /history, /execution-lock | API key / JWT + roles |
| WebHook de flujo | POST /flows/:id/webhook | HMAC-SHA256 X-CSB-Signature |
| Monitoring | /monitoring/pubsub, /tasks, /dlq, /dlq/:topicName, POST /dlq/replay, /workflows/executions, /workflows/:name/sync, /flows/:id/live, /flow-memory, /dashboard | API key / JWT |
| Bindings por ambiente | /api-definitions/:id/bindings[/:env[/rotate]] · /flows/:flowId/bindings-status | API key |
| Internos (OIDC) | /http-executor/execute-with-cert · /internal/flow-memory/* (memorize / recall / forget / sweep) | OIDC de Cloud Workflows |
| Import / export · store | /import, /export · /store/inventory, /store/orders, /store/orders/cancel, /store/orders/fulfillment (simulador de tienda para demos) | API key / JWT |
Puertas de entrada
| Puerta | Quién la usa | Autenticación | Qué dispara |
|---|---|---|---|
| Topic Pub/Sub con schema | Productores con cuenta de servicio (inventario, órdenes, backend Bebbia, Portal de Inventario) | IAM roles/pubsub.publisher sobre el topic; el mensaje se valida contra el schema registrado (Avro en Pub/Sub Schemas o JSON Schema) | Eventarc → Cloud Workflow del flujo cuyo triggerTopic es ese topic; suscripciones pull de los consumers |
WebHook de flujoPOST /api/flows/{flowId}/webhook | Sistemas sin cuenta GCP: tienda Bebbia (CSB-02, CSB-05, CSB-06), Bringoz (CSB-01). Ojo: no da respuesta síncrona de negocio — el veredicto viaja por callback o topic | HMAC-SHA256 del cuerpo crudo con csb-<env>-webhooks-signing-key, header X-CSB-Signature | executeFlow(id, body, 'webhook'): sustituye config por el del flujo, valida el cuerpo contra el inputTemplate y ejecuta el workflow; responde 202 acuse con executionId (el veredicto viaja por callback/topic); 409 sólo si la misma clave de concurrencia (concurrencyKeyField del nodo WebHook, p.ej. customerKey) ya tiene una ejecución activa |
Ejecución manualPOST /api/provisioning/flows/{id}/execute | Personas desde el editor (botón Ejecutar) o scripts con JWT | Login CSB (usuario seed o Google OAuth), roles | Misma ruta que el WebHook, triggeredBy: 'manual' |
Cloud Scheduler (nodo scheduler) | Flujos periódicos | OIDC del job hacia Workflows Executions API | deployFlow crea el job con la expresión cron del nodo |
Disponibilidad por centrosPOST /api/v1/inventory/centers-availability | Quien necesite la disponibilidad de un SKU por centro (CSB-07) | ApiKeyGuard + RolesGuard, header x-api-key — mismo esquema que /api/store/* | executeFlow('consultar-inventario-centros', …, 'inventory-api') y responde síncrono con una fila por centro. Su DTO corta lo que validateExecutionInput deja pasar (sku: "", centers: [], campos de más) con 400 y el mensaje del campo; 502 si el Data Mesh no respondió, 400 si el dominio rechazó, 409 si ese requestId ya tiene una consulta viva |
Simulador de tienda/api/store/* | Demos y pruebas | API key / JWT | Publica inventory.stock.updated.v1, order.placed.v1, order.cancelled.v1, order.fulfillment.requested.v1 |
Envelope que reciben los workflows
Todo camino (topic, WebHook, manual) se normaliza en el prólogo generado normalize_input a args = { metadata, payload }; los pasos leen ${args.payload.<campo>}; las constantes por ambiente (args.payload.config) las fija el CSB desde inputTemplate.config (paso apply_server_config compilado en el deploy y executeFlow), nunca el emisor. Los sagas Terraform (workflows/*.yaml) usan en cambio args.envelope + args.taskMetadata y detectan si vienen de Eventarc (mensaje base64) o de Cloud Tasks.
{
"metadata": {
"flowId": "3f2c…",
"flowName": "alta-cliente-tienda",
"triggeredBy": "webhook",
"triggeredAt": "2026-09-10T15:04:05.000Z",
"correlationId": "REQ-ALTA-0001"
},
"payload": {
"requestId": "REQ-ALTA-0001",
"customerKey": "YXJJC",
"config": { … constantes del ambiente, inyectadas por el CSB … }
}
}
# WebHook firmado (la respuesta es un acuse 202 con executionId; el resultado llega por callback/topic)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOKS_SIGNING_KEY" | awk '{print $2}')
curl -s -X POST "https://csb-dev.rotoplas.com/api/flows/$FLOW_ID/webhook" \
-H "Content-Type: application/json" -H "X-CSB-Signature: $SIG" -d "$BODY"
# Publicar un evento con schema Avro
gcloud pubsub topics publish inventory.stock.updated.v1 --project=rtp-transversal-dev \
--ordering-key=SKU-001 --message="$(cat stock-updated.json)"
Catálogo de topics y schemas
Convención: <dominio>.<entidad>.<evento>.v<n>; cambio incompatible → v2. Avro registrado en Pub/Sub Schemas (política FULL_COMPATIBLE, validado en CI por schema-validation); JSON Schema en el registry del CSB. Los topics base viven en Terraform (infra/envs/dev/main.tf); los de pipelines los crea deployFlow al desplegar el flujo.
| Topic | Schema · ordering | Productor | Consumidores / dispara | Origen |
|---|---|---|---|---|
inventory.stock.updated.v1 | Avro · entity_id (SKU) · DLQ | Servicio de inventario | inventory-to-sap-saga (Eventarc); subs audit, analytics, notification, sap-adapter (push legado) | Terraform |
order.placed.v1 | Avro · order_id · DLQ | Órdenes / tienda | order-to-hubspot-saga; sub push hubspot-adapter (legado) | Terraform |
order.cancelled.v1 | Avro | Órdenes | order-cancellation-notify | Terraform |
order.fulfillment.requested.v1 | JSON Schema · order_id · DLQ | Simulador / tienda | order-fulfillment-orchestrator, order-fulfillment-demo | Terraform + seed |
sap.inventory.synced.v1 · sap.inventory.sync.failed.v1 | Avro · ordering por SKU | ERP adapter y saga | Sin suscripción declarada (brecha) | Terraform (adapter) + seed |
hubspot.order.synced.v1 | JSON Schema | (esperado de order-to-hubspot-saga) | — | seed |
bebbia.checkout.completed.v1, bebbia.cliente.updated.v1, bebbia.pago.registered.v1, bebbia.suscripcion.cancelled.v1, bebbia.consulta.requested.v1, bebbia.instalacion.confirmed.v1 | JSON Schema · suscripcion_id · DLQ | Tienda / backend Bebbia | Un flujo por WS: bebbia-crear-cliente-sap, bebbia-modificar-cliente-sap, bebbia-anticipos-sap, bebbia-cancelar-plan-sap, bebbia-consultas-sap, bebbia-confirmar-instalacion-sap | seed §12 |
sap.cliente.synced.v1, sap.anticipo.registered.v1, sap.plan.cancelled.v1, sap.consulta.completed.v1, sap.instalacion.confirmed.v1, sap.cpi.error.v1 | JSON | Flujos Bebbia → SAP CPI | audit / notification (grafo) | seed §12 |
bebbia.order.paid.v1 | JSON Schema (order_number, store_key, payment_id) · DLQ | CommerceTools (OrderPaymentAdded) | crear-orden-trabajo-bebbia | seed §13 |
bebbia.order.validated.v1 → bebbia.work.order.created.v1, bebbia.first.charge.registered.v1 | JSON | Orquestadores encadenados | fan-out logística + finanzas → apunte en Fluent | seed §13 |
bebbia.installation.times.fetched.v1, bebbia.installation.times.assigned.v1, bebbia.installation.dispatch.failed.v1 | JSON | obtener-horarios, asignar-horario | Backend Bebbia (consumer), reversión de agenda | seed §13 |
bebbia.purifier.order.requested.v1 → assigned.v1 / rejected.v1 | JSON Schema · order_id · DLQ | Tienda Bebbia | pedido-purificador-instalacion → field ops consumer | seed |
inventory.asset.status.changed.v1 → validated.v1 / rejected.v1 → sap|datamesh|portal.updated.v1 / update.failed.v1 | JSON | Portal de Inventario; Bringoz por WebHook | CSB-01: validar-cambio-status-activo → fan-out a SAP ECC, Data Mesh, Portal | seed (CSB-01) |
store.customer.signup.completed.v1 / failed.v1 → notified.v1 / notify.failed.v1 | JSON | CSB-02 recibir-alta-cliente | notificar-alta-tienda → audit / notification | seed (CSB-02) |
delivery.order.accepted.v1 / rejected.v1 | JSON | prueba-entrega-bringoz, mantenimiento-bringoz | audit / notification. El rechazo lleva la etapa donde se cortó (PETICION, TIPO_SERVICIO, ADAPTER, PROVEEDOR) | seed (delivery) |
erp.order.status.retrieved.v1 / failed.v1 | JSON | prueba-estatus-pedido-erp | audit / notification. El fallo lleva la etapa donde se cortó (PETICION, ADAPTER, CONFIGURACION, SAP) | seed (erp) |
delivery.timeslots.quoted.v1, delivery.timeslots.pending.v1, delivery.timeslots.unavailable.v1 | JSON | cotizar-horarios-bringoz, releer-cotizacion-bringoz | pending dispara releer-cotizacion-bringoz (Eventarc, lleva el quoteId); los otros dos van a audit / notification | seed (delivery) |
delivery.timeslot.assigned.v1 / rejected.v1 | JSON | asignar-horario-bringoz | audit / notification. El rechazo del proveedor significa recotizar; no se reencadena solo | seed (delivery) |
store.delivery.order.created.v1 / failed.v1 → notified.v1 / notify.failed.v1 | JSON | CSB-05 recibir-orden-entrega | notificar-orden-entrega-tienda → audit / notification. El fallo lleva errorCode (DATOS_INCOMPLETOS, ADAPTER_NO_DISPONIBLE, BRINGOZ_RECHAZO) | seed (CSB-05) |
store.delivery.slots.quoted.v1 / unavailable.v1 → notified.v1 / notify.failed.v1; store.delivery.slots.pending.v1 | JSON | CSB-06 cotizar-slots-entrega | notificar-slots-tienda → audit / notification. pending es terminal aquí: lleva el quoteId con el contrato de releer-cotizacion-bringoz y lo recoge CSB-09 | seed (CSB-06) |
csb.health.ping.v1 · csb.hello.world.v1 | Avro · JSON | Cloud Scheduler · hello-world-flow | Healthcheck del bus (csb-healthcheck.sh) · validación mínima | Terraform · seed |
Ejemplo · inventory.stock.updated.v1 (Avro schemas/inventory.stock.updated.v1.avsc)
{
"event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"entity_id": "SKU-001",
"sku_code": "SKU-001",
"warehouse_id": "WH01",
"quantity_before": 120,
"quantity_after": 100,
"unit": "units",
"updated_at": 1788000000000,
"source_system": "inventory-service",
"correlation_id": "corr-7f3a"
}
Catálogo de flujos
Dos capas que nunca se pisan: la IaC base (workflows YAML en workflows/ desplegados por Terraform, módulo csb-workflow) y los flujos dinámicos del editor (grafo en Postgres, desplegados por deployFlow con etiqueta managed_by=csb-deployflow). El seed (apps/api/src/database/seed.ts y seed-data/*) registra ambos en el catálogo para que se vean en /flows.
Sagas Terraform (workflows/*.yaml)
| Workflow | Disparador | Pasos | Resultado | Estado |
|---|---|---|---|---|
inventory-to-sap-saga | inventory.stock.updated.v1 (Eventarc) o Cloud Tasks | validar → POST {SAP_ADAPTER_URL}/transform → POST /sync (espera taskId) → poll GET /sync/{id}/status (300 s) | sap.inventory.synced.v1; en fallo sap.inventory.sync.failed.v1 + DLQ + compensate-inventory-sync | roto el ERP adapter no expone /transform ni status |
compensate-inventory-sync | Invocado por la saga | GET /sync/{id}/status → rollback si PARTIAL → notificar operaciones | — | roto rutas inexistentes |
order-to-hubspot-saga | order.placed.v1 | validar → /contacts/resolve → /deals/transform → /deals/sync | retorna { status, hubspotDealId }; en fallo DLQ | roto el CRM adapter solo tiene /sync |
order-cancellation-notify | order.cancelled.v1 | /deals/sync con envelope <id>-cancel | — | roto |
order-fulfillment-orchestrator | order.fulfillment.requested.v1 | reserva SAP (/sync, compensación RELEASE_RESERVATION) → contacto → deal (compensación CANCEL_DEAL) | DLQ propia en fallo | roto contratos de /sync no coinciden |
send-notification-workflow | csb-notification-consumer | notificación de reorden (log-only en dev) | — | ok |
Flujos del editor (seed)
| Flujo | Disparador | Qué hace | Sistemas | Verificación |
|---|---|---|---|---|
crear-orden-trabajo-bebbia (CreateWorkOrder v176) | bebbia.order.paid.v1 | 4 orquestadores: validar-orden (dedup Fluent por ref, orden CT, cliente Fluent, programa G3V) → fan-out logistica-fluent-bringoz (createOrder(AndCustomer), orden de campo Bringoz con compensación, mantenimiento, add-on G3V) y suscripcion-y-primer-cargo-sap (suscripción MySQL vía Apigee, SAP_ID, WS3 Anticipos con compensación, log) → registrar-apunte-en-fluent | Fluent, CommerceTools, Bringoz, Apigee, CF, Add-on, SAP CPI | 7 escenarios SUCCEEDED (2 sep) contra mocks |
obtener-horarios-instalacion (GetTimes v100) | API trigger | token Bringoz → depots → opciones de horario de {idFluent}-{Kindflag} | Bringoz | ok (mock) |
asignar-horario-instalacion (AssignTimes v32) | API trigger | asignar-horario (mantenimiento status 9, reserva de equipo, dispatch con compensación, warehouse en Fluent, slot, correo, motivo) → si falla revertir-agenda-y-reconsultar-horarios | CF, Bringoz, Fluent, Backend, SendGrid | ok (mock) |
pedido-purificador-instalacion | bebbia.purifier.order.requested.v1 | disponibilidad on-hand en Fluent → orden + reserva + instalador y alta WS1; sin stock publica rejected | Fluent, SAP CPI | ok (mock) |
actualizar-status-activo-inventario (CSB-01) | inventory.asset.status.changed.v1 o WebHook (Bringoz) | validar (serie, catálogo, idempotencia por lastEventId de Data Mesh, cierre en Bringoz) → fan-out SAP ECC (movimiento con reverso), Data Mesh (ingesta), Portal (PATCH) | Bringoz, SAP ECC, Data Mesh, Portal | 7 escenarios SUCCEEDED (2 sep); contratos reales pendientes |
alta-cliente-tienda (CSB-02) | WebHook firmado de la tienda | recibir-alta-cliente (validación, estado normalizado, candado en el registro propio customer_signups vía /internal/customer-signups/{customerKey}, ficha GET /v2/customers/{customerKey}, WS1, registro del customerSap antes de publicar) → notificar-alta-tienda (callback único para éxito y error, propuesto a Bebbia) | Tienda Bebbia (API v2, x-rotoplas-api-key), SAP CPI, CSB | Revisión Bebbia 9 sep atendida (registro propio, 409 por customerKey, 202 acuse, config del servidor); callback y API key pendientes de Bebbia |
prueba-entrega-bringoz | Ejecución manual desde el editor (contrato = inputTemplate) | validar externalId/lineId → GET {DELIVERY}/health (OIDC) → POST {DELIVERY}/delivery/create-order, que el adapter traduce a PUT /v2/oms/orders-v2 de Bringoz | Delivery adapter (rtp-delivery-adapter); Bringoz sólo detrás de él | delivery.order.accepted.v1 si Bringoz acepta; si no, delivery.order.rejected.v1 con la etapa (PETICION, ADAPTER, PROVEEDOR). Prueba de la conexión CSB → adapter → Bringoz |
prueba-estatus-pedido-erp | Ejecución manual desde el editor (contrato = inputTemplate) | validar cuenta de crédito y rango → GET {ERP}/health (OIDC) → POST {ERP}/erp/orders/status, que el adapter traduce a get_so_sts de SAP CPI. Sólo lectura: no cambia nada en el ERP, se puede repetir | ERP adapter (rtp-erp-adapter); SAP CPI sólo detrás de él | erp.order.status.retrieved.v1 con los pedidos y su total; si no, erp.order.status.failed.v1 con la etapa (PETICION, ADAPTER, CONFIGURACION = faltan los secretos csb-<env>-sap-cpi-order-status-*, SAP). Prueba de la conexión CSB → adapter → SAP |
mantenimiento-bringoz (CreateWOMaintenance v105) | Ejecución manual desde el editor (contrato = inputTemplate) | validar petición y tipo de servicio → GET {DELIVERY}/health → alta con POST {DELIVERY}/delivery/create-order. La tabla config.mapeoServicio (nueve tipos: Basico, Robusto, Primario, Membrana, Correctivo, Reubicacion, ReinstalacionGrifoReposicion, CambioTecnologia, RecuperacionPorInactividad) decide purpose, type y si la línea lleva destinationTask o pickupTask. Paso try: si la llamada revienta con la orden ya creada, compensa con POST {DELIVERY}/delivery/cancel/{orderId} | Delivery adapter (rtp-delivery-adapter); Bringoz sólo detrás de él | delivery.order.accepted.v1 o delivery.order.rejected.v1 con la etapa (PETICION, TIPO_SERVICIO, ADAPTER, PROVEEDOR). El externalId va sin sufijo de ejecución: repetirlo devuelve el 400204 de Bringoz, que es su candado de idempotencia |
cotizar-horarios-bringoz (GetTimes v100) | Ejecución manual desde el editor (la orden y su línea ya existen en Bringoz) | GET {DELIVERY}/health → depots de la línea (POST {DELIVERY}/delivery/{orderId}/lines/{lineId}/depots, el adapter ya filtra por areaIds y externalId de 8 chars) → cotización sobre los 5 primeros (.../time-slots). El tope de 5 se enumera con una escalera de condiciones: Cloud Workflows no sabe recortar una lista | Delivery adapter; Bringoz sólo detrás de él | delivery.timeslots.quoted.v1 con las ventanas, delivery.timeslots.pending.v1 si el quote asíncrono sigue abierto, o delivery.timeslots.unavailable.v1 con la etapa (PETICION, ADAPTER, DEPOTS, SIN_VENTANAS) |
releer-cotizacion-bringoz (GetTimes _API_3) | delivery.timeslots.pending.v1 | GET {DELIVERY}/delivery/{orderId}/lines/{lineId}/time-slots/{quoteId}. Una sola relectura por ejecución, no un bucle: el constructor de sagas no tiene contador con el que cortar un quote eterno | Delivery adapter; Bringoz sólo detrás de él | delivery.timeslots.quoted.v1 si el quote cerró con ventanas; si no, delivery.timeslots.unavailable.v1 con etapa COTIZACION_PENDIENTE (relanzarlo con el mismo quoteId) o SIN_VENTANAS |
asignar-horario-bringoz (AssignTimes v32) | Ejecución manual desde el editor (con el optionId de la ventana elegida) | GET {DELIVERY}/health → POST {DELIVERY}/delivery/{orderId}/lines/{lineId}/dispatch/{optionId}, que el adapter traduce al único endpoint de Bringoz que cuelga de /v2/oms/orders/ sin -v2 y va sin cuerpo. El 404 que Bringoz manda con HTTP 200 lo normaliza el adapter: el CSB no busca subcadenas en el cuerpo | Delivery adapter; Bringoz sólo detrás de él | delivery.timeslot.assigned.v1 o delivery.timeslot.rejected.v1 con la etapa (PETICION, ADAPTER, PROVEEDOR). El rechazo del proveedor significa recotizar, pero el CSB no reencadena solo para no realimentar el ciclo |
crear-orden-entrega-tienda (CSB-05) | WebHook firmado de la tienda (candado por orderId) | recibir-orden-entrega (validar petición y datos que Bringoz exige → candado en la tienda GET /v2/orders/{orderId}/delivery: si ya trae bringozOrderId no se vuelve a crear, porque el 400204 del duplicado llega como rejected y sería indistinguible de un rechazo de negocio → GET {DELIVERY}/health → POST {DELIVERY}/delivery/create-order) → notificar-orden-entrega-tienda (callback único para éxito y error). El externalId lo compone el CSB como {orderId}-{kindFlag}: la convención de Bringoz no se filtra al contrato de la tienda | Tienda Bebbia (x-rotoplas-api-key), Delivery adapter; Bringoz sólo detrás de él | store.delivery.order.created.v1 (CREATED / ALREADY_EXISTS) o failed.v1. Los cuatro endpoints de la tienda son PROPUESTA pendiente de Bebbia |
cotizar-slots-tienda (CSB-06) | WebHook firmado de la tienda (candado por orderId) | cotizar-slots-entrega (GET {DELIVERY}/health → almacenes disponibles POST {DELIVERY}/delivery/{orderId}/lines/{lineId}/depots → cotización sobre los 5 primeros con la misma escalera de tamaños 1..5) → notificar-slots-tienda. Requiere que la orden ya exista en Bringoz (la crea CSB-05) | Tienda Bebbia, Delivery adapter; Bringoz sólo detrás de él | store.delivery.slots.quoted.v1, unavailable.v1 (SIN_ALMACENES / SIN_VENTANAS / …) o pending.v1. El quote asíncrono NO se resuelve aquí: el polling es CSB-09 |
consultar-inventario-centros (CSB-07) | WebHook firmado (candado por requestId) · ejecución manual desde el editor | Una sola caja del blueprint: "Validacion inventario Centros" → "Valida inventario en Centros" del Core Data Mesh; la reutilizan checkout y mantenimiento. Las cajas vecinas no son de este ticket: los centros llegan ya resueltos y la cotización de slots consume el resultado después. Un solo paso: POST {DATAMESH}/inventory/centers-availability (DM-02) con {sku, centers[]} — centros por centerId de 4 chars, de 1 a N, sin tope — con el BEARER que ya usa CSB-01. Sin guarda (la hace validateExecutionInput) y sin condición de éxito: al ser el único result, el workflow devuelve la respuesta del dominio tal cual. Candado de concurrencia por requestId, no de idempotencia: es una lectura | Core Data Mesh (EDM) | Ninguno — es REQUEST_REPLY: responde a quien lo llamó con el sobre {data, error, meta}, una fila por centro con available, availableQuantity (incluye reacondicionado, HU-00-08) y restock sólo cuando no hay stock |
bebbia-*-sap (6 flujos, seed §12) | bebbia.*.v1 | Mapeo crudo de cada WS CPI (WS1, WS2, WS3, WS5, WS7, WS11) → topic sap.*.v1 o sap.cpi.error.v1 | SAP CPI gateway | ok (mock) |
inventory-to-sap-saga, order-to-hubspot-saga, order-cancellation-notify (grafos ACTIVE) | ver arriba | Representación en el editor de los sagas Terraform, con nodo adapter csb-sap-adapter / csb-hubspot-adapter | adapters | ver estado arriba |
order-fulfillment-demo · hello-world-flow | manual · csb.hello.world.v1 | Demo con los cinco tipos de nodo (estatus de pedidos SAP de sólo lectura por POST /erp/orders/status + HubSpot con compensación) · ping a /api/health | adapters · csb-api | demo |
/flows/:id el grafo muestra disparador → orquestadores → APIs externas → topics de salida → consumers. Cada orquestador documenta su contrato de entrada en data.inputTemplate; executeFlow valida el payload contra él antes de correr un solo paso. Los pasos publish cierran una etapa entregando el inputTemplate de la siguiente.
Adapters y sistemas externos
Adapters (repos independientes)
| Adapter | Cloud Run · cola · topic | Cómo lo alcanza el CSB | Identidad | Documentación |
|---|---|---|---|---|
| ERP SAP CPI + OData | csb-erp-adapter · csb-erp-adapter-queue · sap.inventory.synced.v1 | remote state csb-erp-adapter → local.adapters.erp → ERP_ADAPTER_URL; Cloud Tasks (SAP_ADAPTER_QUEUE) o http.post OIDC | csb-api actúa como csb-erp-adapter-tasks-sa | /adapters/erp/docs |
| CRM HubSpot | rtp-transversal-dev-dev-crm-adapter · csb-hubspot-adapter-queue-dev · csb-crm-results-dev | remote state crm-adapter/dev → CRM_ADAPTER_URL; Cloud Tasks (HUBSPOT_ADAPTER_QUEUE) o OIDC | el adapter otorga run.invoker a csb-run-sa (no coincide con sa-csb-api-dev) | /adapters/crm/docs |
| Delivery Bringoz / Nexus | csb-delivery-adapter-dev-api · (sin cola ni topic en outputs) | remote state → DELIVERY_ADAPTER_URL → ApiDefinition CSB Delivery Adapter; los pasos llaman http.* con auth: OIDC (no hay cola todavía) | el workflow corre como sa-csb-api-dev (run.invoker de proyecto); para Cloud Tasks, csb-api actúa como csb-dlv-adapter-dev-tasks-sa | /adapters/delivery/docs |
Cableado (commit 40352ce): infra/envs/dev/main.tf lee los outputs de cada adapter con terraform_remote_state, arma local.adapters (url, cola, SA de tasks, topic) y lo inyecta en csb-api y en el job csb-api-migrate como ERP_ADAPTER_URL, CRM_ADAPTER_URL, DELIVERY_ADAPTER_URL; el seed registra con ellas las ApiDefinitions y los Adapters (csb-sap-adapter, csb-hubspot-adapter, csb-delivery-adapter). Output adapter_endpoints muestra lo resuelto. Delivery ya está conectado de punta a punta: DELIVERY_ADAPTER_URL resuelve el token {{DELIVERY}} de apps/api/src/database/seed-data/delivery-adapter-pipelines.ts, que siembra la ApiDefinition del adapter (authType NONE: la credencial de Bringoz vive en el adapter, no en el CSB) y los cinco flujos que hablan con Bringoz por él (prueba-entrega-bringoz, mantenimiento-bringoz, cotizar-horarios-bringoz, releer-cotizacion-bringoz, asignar-horario-bringoz). ERP arrancó el mismo camino (14 sep 2026): ERP_ADAPTER_URL resuelve el token {{ERP}} de apps/api/src/database/seed-data/erp-adapter-pipelines.ts, que siembra la ApiDefinition CSB ERP Adapter (authType NONE, misma razón) con las rutas canónicas /erp/*. Su primer flujo es prueba-estatus-pedido-erp (17 sep 2026), el equivalente de prueba-entrega-bringoz para SAP: estatus de pedidos (POST /erp/orders/status → CPI get_so_sts), una consulta síncrona de sólo lectura que no necesita saga ni compensación, y cuyo desenlace dice qué salto de la cadena falló. Para que responda en un ambiente, ese ambiente necesita los secretos csb-<env>-sap-cpi-order-status-{url,user,password}: sin ellos el adapter responde 503 y el flujo publica la etapa CONFIGURACION. Cada portal de adapter tiene la sección "Integración con el CSB" con el detalle y las brechas.
APIs externas registradas (ApiDefinitions del seed)
| ApiDefinition | Auth · secreto (csb-<env>-…) | Usada por | Base URL (variable) |
|---|---|---|---|
| Fluent Commerce OMS | BASIC → OAuth2 password grant · fluent-credential | crear-orden-trabajo, asignar-horario, purificador | FLUENT_API_URL |
| CommerceTools Orders API | BASIC client_credentials · commercetools-credential | validar-orden | COMMERCETOOLS_API_URL / _AUTH_URL / _CLIENT_ID |
| Bringoz Logistics v2 | API key x-api-key → token · bringoz-credential | logística, horarios, dispatch, CSB-01 | BRINGOZ_API_URL |
| Apigee Gateway Bebbia · Bebbia Cloud Functions · Bebbia Backend · SendGrid CF · Cancel Reason CF · SAP First Charge Logger · Bebbia Add-on API | BASIC / NONE / API key · apigee-credential, bebbia-addon-credential | Pipelines Bebbia | APIGEE_API_URL, BEBBIA_*_URL |
| SAP CPI bebbia Gateway | BASIC · sap-cpi-user + sap-cpi-password | bebbia-*-sap, CSB-02, purificador, WS3 del checkout | SAP_CPI_BASE_URL |
| Portal de Inventario Rotoplas · Core Data Mesh Rotoplas · SAP ECC Inventario | API key · BEARER · BASIC · portal-inventario, data-mesh, sap-ecc | CSB-01 | INVENTORY_PORTAL_API_URL, DATA_MESH_API_URL, SAP_ECC_API_URL |
| Tienda Bebbia Clientes (Bebbia Backend API v2) | Service API key x-rotoplas-api-key · tienda-bebbia | CSB-02 | BEBBIA_STORE_API_URL |
| Tienda Bebbia Entregas (estado de la entrega y los dos callbacks) | Service API key x-rotoplas-api-key · reutiliza tienda-bebbia, no crea secreto nuevo | CSB-05, CSB-06 | BEBBIA_STORE_API_URL |
CSB Registro de altas SAP (/internal/customer-signups) | OIDC (el propio workflow) | CSB-02 candado de idempotencia | API_URL |
CSB Delivery Adapter (/delivery/*) | OIDC (el propio workflow) · sin secreto en el CSB: las credenciales de Bringoz (bringoz-host, bringoz-basic-auth, bringoz-tenant-id, bringoz-site-id, bringoz-account-id) las lee el adapter | prueba-entrega-bringoz, mantenimiento-bringoz, cotizar-horarios-bringoz, releer-cotizacion-bringoz, asignar-horario-bringoz; y los dos flujos con puerta de la tienda: crear-orden-entrega-tienda (CSB-05) y cotizar-slots-tienda (CSB-06). Destino de la migración de los pasos que hoy llaman a Bringoz directo | DELIVERY_ADAPTER_URL |
CSB ERP Adapter (/erp/*) | OIDC (el propio workflow) · sin secreto en el CSB: las credenciales de SAP CPI (sap-cpi-host, sap-cpi-user, sap-cpi-password, y para estatus de pedidos sap-cpi-order-status-*) las lee el adapter | Estatus de pedidos (POST /erp/orders/status, CPI get_so_sts); destino de la migración de los pasos que hoy llaman al gateway CPI directo (WS1, WS3, WS5, WS7, WS11) | ERP_ADAPTER_URL |
| SAP S/4HANA OData API · HubSpot CRM v3 API | OAuth2 / BEARER · sap-api-credentials, hubspot-api-token | Sagas de inventario y órdenes (vía adapters) | ERP_ADAPTER_URL, CRM_ADAPTER_URL |
En dev todas apuntan a mocks del docker-compose (mock-sap-api, mock-hubspot-api, mock-bringoz-api, mock-bebbia-integrations, mock-inventory-integrations…). En QA/PRD la URL real entra por binding de ambiente (baseUrlOverride) y el secreto por bootstrap-secrets.sh.
Consumers de coreografía
| Consumer | Suscripción | Destino | Idempotencia |
|---|---|---|---|
csb-audit-consumer | inventory.stock.updated.v1.audit.sub (pull, min 1 instancia) | Tabla audit_log en Cloud SQL Postgres (no Firestore: el proyecto es multi-tenant) | Doc ID = event_id; redelivery sobrescribe |
csb-analytics-consumer | inventory.stock.updated.v1.analytics.sub (pull) | BigQuery csb_analytics.csb_events, particionada por received_at | Query por event_id + insertId = event_id |
csb-notification-consumer | inventory.stock.updated.v1.notification.sub (pull) | send-notification-workflow cuando quantity_after < reorder_point (default 10) | Ack siempre; evita reprocesar |
Contrato compartido: el envelope debe traer id, type y source; un mensaje sin ellos se nack()ea a la DLQ. entityid es opcional pero recomendado. Un consumer nuevo sigue el playbook AE-42: módulo Terraform csb-subscription (sub + DLQ + SA), deserialización Avro e idempotencia por event_id. Los edges subscribe del editor hacia audit/analytics/notification apuntan a estos servicios reales.
Editor visual y deployFlow
Nodos y conexiones válidas
topic→ adapter, consumer, workflowworkflow→ adapter, topic, external-apiadapter→ external-api, topicexternal-api→ adapter ·consumeres terminal- Otros:
start/end-event,gateway(condición),webhook(disparador firmado),scheduler(cron)
Topic, adapter y external-api deben existir antes en el registry (el nodo guarda un refId). Con un nodo workflow y 2+ adapters/consumers el flujo es SAGA; con uno, REQUEST_REPLY; sin workflow, EVENT_DRIVEN.
Pasos del saga builder
call(HTTP con credencial del catálogo oauth: OIDC,result,resultFilter,retryPolicy,pathParamValues)try= call +compensationCallcondition(sintaxis Cloud Workflows:and/or/not) contrueNextId/falseNextId;endtermina con éxitopublish(topic + payload = contrato de la siguiente etapa)memorize/recall/forget: memoria compartida entre pasos y ejecuciones (/internal/flow-memory, sweep de expiración)
Ciclo de vida
- Guardar —
PUT /flows/:id/graph - Preview GCP —
GET /provisioning/flows/:id/preview: qué se crea, qué se salta (managed_by=terraform), costo estimado - Deploy —
POST …/deploy: topics, DLQs, suscripciones (push a<adapter>/eventsen edges topic→adapter), colasque-<flow>-<adapter>, workflows compilados a YAML, triggers Eventarc, jobs Scheduler; snapshot inmutable conversion - Ejecutar —
POST …/execute; una sola ejecución activa por flujo (409) - Historial / redeploy —
GET …/history,POST …/redeploy/:deploymentIdcrea una versión nueva (rollback v3→v1 crea v4)
Guardas
- Anti-colisión: nunca sobrescribe un recurso con
managed_by=terraform; colas con prefijoque-como señal - Bindings por ambiente: el grafo guarda
logicalKey; el deploy falla rápido si falta el binding delCSB_ENV - Edición concurrente: presencia y 409 en vez de last-write-wins
MOCK_GCP=trueen local: simula recursos y ejecuta los pasos con un walker que sigue solo los topics realmente publicados
Guía paso a paso con capturas del ciclo completo: docs/MANUAL-FLOW-WALKTHROUGH.md; data binding entre pasos: docs/DISENO-DATA-BINDING-ENTRE-PASOS.md; credenciales por componente: docs/DISENO-CREDENCIALES-POR-COMPONENTE.md.
Contratos, fallos e idempotencia
Tipos de contrato
- Eventos: Avro
.avscenschemas/, registrados en Pub/Sub Schemas; evoluciónFULL_COMPATIBLE(docs/contracts/AVRO-EVOLUTION.md) validada en CI. - API control plane: OpenAPI 3.1 exportado con
npm run openapi:exportadocs/contracts/csb-api.openapi.yaml, lint Spectral en CI; en vivo en Swagger UI. - Adapters: OpenAPI en cada repo (Swagger de cada portal) y contratos de Cloud Tasks en
packages/shared/src/contracts/cloud-tasks.contracts.ts(SyncPayloadDto, headersX-Task-Queue,X-Retry-Count,X-Workflow-Execution, OIDC). - Orquestación: YAML de Cloud Workflows en
workflows/; estándar de I/O{ metadata, payload }→{ code 200/500 }.
Qué pasa cuando un flujo falla
- Error transitorio (429, 5xx, timeout):
retryPolicydel paso; defaultmaxAttempts 3, 1 s → 8 s ×2conhttp.default_retry_predicate. Nunca se reintenta la compensación. - Error de negocio (4xx) o retries agotados:
compensationCallsi el paso estry; la ejecución quedaFAILED. No hay resume. - Relanzar crea una ejecución nueva desde el paso 1 con el mismo payload y
event_id; por eso los pasos que escriben en SAP/HubSpot deben ser idempotentes (upsert porevent_ido clave de negocio). - Mensajes irrecuperables van a la DLQ del topic; se reprocesan con
POST /api/monitoring/dlq/replayoscripts/csb-dlq-replay.sh.
// Contrato de resultado de una ejecucion (workflow_executions)
{
"status": "SUCCESS", // SUCCESS | FAILED
"eventId": "a1b2c3d4-…",
"steps": [
{ "id": "alta-s04", "status": "SUCCEEDED", "durationMs": 212 },
{ "id": "alta-s06", "status": "SUCCEEDED", "attempts": 2 }
],
"published": [ "store.customer.signup.completed.v1" ]
}
Seguridad e identidad
Borde
- Load balancer HTTPS global (
csb-dev.rotoplas.com, certificado administrado, TLS ≥ 1.2) con URL map:/api/*→ csb-api,/adapters/<x>/docs*→ adapter (solo ese prefijo), resto → csb-web. - Cloud Armor: deny de scanners por User-Agent, deny de rutas de exploit, rate limit por IP, WAF OWASP con parseo JSON.
- Todos los Cloud Run con ingress
INTERNAL_LOAD_BALANCER(política de organización): la URL*.run.appresponde 404 desde internet.
Identidades (least privilege, docs/security/IAM-ROLE-MATRIX.md)
sa-csb-api-<env>: pubsub.editor, workflows.editor, eventarc.developer, cloudtasks.admin, cloudsql.client, run.invoker, secretAccessor por secreto, KMS por key, actAs sobre sí misma y sobre las*-tasks-sade los adapters.- Una SA por workflow (módulo
csb-workflow), por consumer (csb-subscription) y por adapter; sin JSON keys, Workload Identity. - Usuarios: JWT (login seed o Google OAuth), roles ADMIN/VIEWER (
RolesGuard), API keyx-api-keypara integraciones; rutas internas de Workflows con OIDC (OidcAuthGuard). La api key válida es el secretocsb-<env>-internal-api-key, que Cloud Run inyecta comoAPI_KEYSeINTERNAL_API_KEY:ApiKeyGuardlee las dos y falla cerrado — si no hay ninguna clave configurada rechaza, no acepta. Quien entra por api key no lleva rol yRolesGuardlo trata como service account de confianza, así que la clave vale tanto como una sesión de admin.
Secretos
- Secret Manager
csb-<env>-<name>:db-*,jwt-secret,internal-api-key,webhooks-signing-key,hubspot-api-token,sap-*, credenciales de pipelines*-credential; estructura en Terraform, valores porscripts/bootstrap-secrets.sh. - KMS (
csb-<env>-api-credentials) para el DEK que envuelve credenciales guardadas enApiDefinition(opcional). - Headers custom, certificados mTLS y valores planos vs Secret Manager por paso (
/http-executor/execute-with-cert).
WebHooks entrantes
HMAC-SHA256 sobre el cuerpo crudo (rawBody) con WEBHOOKS_SIGNING_KEY, comparación en tiempo constante; sin firma o inválida → 401. El nodo webhook del grafo documenta el contrato campo por campo y executeFlow lo valida contra el inputTemplate.
Infraestructura y CI/CD
Terraform (infra/envs/{dev,qa,prod} + módulos)
| Módulo | Recursos |
|---|---|
csb-compute | Cloud Run csb-api, csb-web (+ sap-adapter y hubspot-adapter internos, legado), SAs, env vars, extra_env con URLs de adapters, actAs sobre *-tasks-sa |
csb-topic · csb-subscription · csb-tasks-queue · csb-workflow | Topics con schema y DLQ; subs pull/push con SA y DLQ; colas csb-sap-queue / csb-hubspot-queue; workflows con SA propia y trigger Eventarc opcional |
csb-database · csb-secrets · csb-kms · csb-iam-prerequisites | Cloud SQL Postgres 16; secretos csb-<env>-* con accesos por SA; key ring; grants previos |
csb-networking · csb-loadbalancer · csb-armor · csb-artifact-registry · csb-monitoring · csb-org-policy | VPC y peering privado; LB HTTPS con NEGs serverless (web, api, adapters docs); política Cloud Armor; repo csb-adapters; alertas; políticas de organización (no aplicadas por decisión AE-135) |
Consumers (en envs/dev/main.tf) | Cloud Run audit / analytics / notification con VPC access y min instances |
Pipeline Cloud Build (cloudbuild.yaml, push a dev)
- install · lintModo dev-fast:
security-audit,test(jest de api se cuelga en teardown),openapi-lint,schema-validationyscan-*comentados; re-habilitar en qa/prod. - build-api · build-web · build-{audit,analytics,notification}-consumerImágenes en paralelo, etiquetadas con
$SHORT_SHA; push a Artifact Registrycsb-adapters. - tf-init → tf-import → tf-plan → tf-applyEn
infra/envs/$_ENV; importa secretos preexistentes; la imagen se ignora (lifecycle.ignore_changes). - deploy-* con --no-trafficCinco servicios; startup probe de csb-api ampliado (~3 min).
- smoke-test
scripts/csb-smoke-test.sh. - traffic-* --to-latestSolo si el smoke test pasa.
- migrate-seed · deploy-summaryJob
csb-api-migrate: migraciones TypeORM + seed con las URLs de adapters; resumen del despliegue. Timeout total 40 min,E2_HIGHCPU_8.
apps/api/src/docs/public/; npm run build (tsc + scripts/copy-docs-assets.js) los copia a dist/docs/public/ y viajan en la imagen de csb-api. Activo fuera de production; DOCS_ENABLED=true|false lo fuerza.
Monitoreo y runbook
Dónde mirar
/monitoring/overview,/topics,/queues,/workflows,/dlq,/adapters,/connectorsen csb-web.- API:
GET /api/monitoring/pubsub(backlog por sub),/tasks(colas),/workflows/executions,/flows/:id/live,/dlq,/flow-memory,/dashboard. - Health:
/api/health(versión y env),/api/health/ready; bus:scripts/csb-healthcheck.sh(round-trip Pub/Sub vía Cloud Scheduler). - Cloud Logging:
resource.labels.service_name="csb-api"; ejecuciones en Workflows conGOOGLE_CLOUD_WORKFLOW_EXECUTION_ID.
Mensajes en DLQ
# listar y reprocesar
curl -s -H "x-api-key: $KEY" https://csb-dev.rotoplas.com/api/monitoring/dlq
curl -s -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
https://csb-dev.rotoplas.com/api/monitoring/dlq/replay \
-d '{"topicName":"order.placed.v1.dlq","maxMessages":50}'
# o bash scripts/csb-dlq-replay.sh
Un flujo quedó FAILED
- Abrir
/flows/:id→ Historial → ver el paso que falló y el error de Cloud Workflows. - Si fue transitorio y ya se agotaron los retries: Relanzar (mismo payload y
event_id). - Si fue 400 de bindings (
faltan bindings de credenciales para el ambiente): crear el binding conPUT /api/api-definitions/:id/bindings/:envy cargar el secreto con/rotate. - Si el YAML apunta a URLs de DEV en otro ambiente: falta
baseUrlOverrideen el binding.
Deploy que no toma tráfico
- Ver el startup probe de csb-api (migraciones largas) y los logs de la revisión; el pipeline solo promueve tras el smoke test.
- Terraform y
gcloud run deploydesplegando la misma imagen provocaba doble revisión: hoy Terraform ignora la imagen. csb-apinecesita reinicio para tomar variables nuevas del seed (BEBBIA_STORE_API_URL,DATA_MESH_API_URL…).
Promoción DEV → QA → PRD
Directriz (24 ago 2026): INSTALL.md es el documento central; QA se limpia antes de promover y PRD se instala en un solo paso. El grafo de un flujo es portátil: guarda logicalKey por API y la resolución a secreto y URL reales ocurre en deploy con la tabla environment_bindings.
- Verificar bindings del ambiente destino
GET /api/flows/:flowId/bindings-status; crear los faltantes conPUT /api/api-definitions/:id/bindings/:env(secretRef,baseUrlOverride). - Cargar los secretos
bootstrap-secrets.shoPOST …/bindings/:env/rotate; nombrescsb-<env>-*. - Desplegar en el ambiente destinoTerraform en
infra/envs/<env>conCSB_ENVcorrecto; deploy de los flujos desde el editor. - VerificarHealth de los cinco servicios, ejecución de prueba de cada flujo, backlog cero en DLQs. Checklist completo en
docs/PLAYBOOK-PROMOCION-FLUJOS.mdeINSTALL.md§11.
Desarrollo local y pruebas
Levantar todo
cp .env.example .env # JWT_SECRET obligatorio; MOCK_GCP=true
docker compose up -d --build # api :3000 · web :3001 · postgres · emuladores Pub/Sub y Tasks · mocks
docker compose --profile seed run seed
open http://localhost:3001 # login: c.hcruz@rotoplas.com / SEED_ADMIN_PASSWORD
curl -s http://localhost:3000/health
open http://localhost:3000/docs # este portal · Swagger en /docs/api
Integración con los adapters
Desde la carpeta padre rotoplas/, docker-compose.integration.yml levanta CSB + los tres adapters (:8081 ERP, :8082 CRM, :8083 Delivery) + mocks; integration-smoke-test.sh recorre el flujo completo. Ver INTEGRATION.md.
Pruebas
cd apps/api && npx jest --forceExit # 542; sin --forceExit se cuelga en teardown
cd apps/web && npm test && npm run typecheck
cd apps/<consumer> && npm test
npm run openapi:export # regenera docs/contracts/csb-api.openapi.yaml
Convenciones
feature/*→dev→main; nunca push directo amain; nunca prod sin confirmación humana.- Conventional Commits; todo recurso GCP en Terraform o
deployFlowcon etiquetas; secretos solo en Secret Manager. - Todo proceso orquestado expuesto como API (patrón
flows+provisioning+monitoring).
Pendientes y brechas
- bloqueanteLos sagas Terraform no pueden llegar a los adapters externos. El módulo
csb-workflowno declarauser_env_vars(SAP_ADAPTER_URL,HUBSPOT_ADAPTER_URLquedan vacías), las SAs de los workflows no tienenrun.invokeren los adapters, y los YAML esperan rutas (/transform,/sync/{id}/status,/contacts/resolve,/deals/*) que los adapters no exponen. Decidir un modelo de contrato (recomendado: síncronoPOST /<dominio>/<acción>conSyncPayloadDto) y aplicarlo. - legadoAdapters internos en
csb-compute.sap-adapteryhubspot-adaptersiguen declarados con suscripciones push (inventory.stock.updated.v1,order.placed.v1); retirarlos y apuntar alocal.adapters.*. - deudaWeb services Bebbia directos a CPI. WS1 ya migró: CSB-02 llama
POST {{ERP}}/erp/customersen el ERP adapter con OIDC (API CSB ERP Adapter, baseERP_ADAPTER_URL), así que el host del gateway y el secretosap-cpi-tokensalieron de ese flujo. Faltan WS3 (CSB-03 primer cargo), WS11 (CSB-04 instalación) y los mapeos crudos deseed.ts§12, que siguen usando{{SAPCPI}}con la credencial del catálogo. - deudaBringoz directo en los flujos Bebbia. El delivery adapter ya está registrado en el seed (Adapter
csb-delivery-adapter, ApiDefinitionCSB Delivery Adapter) y ya tiene los cinco flujos que traducen las integraciones documentadas de Bebbia (alta de entrega, mantenimiento con compensación, cotización de horarios, relectura del quote y asignación del slot), pero los pipelines de logística y agendado debebbia-pipelines.tssiguen llamando a{{BRINGOZ}}(BRINGOZ_API_URL, sin valor eninfra/: en dev apuntan al mock inexistentemock-bringoz-api). Migrar esos pasos a{{DELIVERY}}conauth: OIDC. El adapter todavía no expone cola ni topic de resultados en sus outputs, así que la vía Cloud Tasks sigue pendiente. - deudaResultados sin consumidor.
sap.inventory.synced.v1ycsb-crm-results-devno tienen suscripción del CSB;hubspot.order.synced.v1nadie lo publica; edges topic→adapter provisionan push a/events, ruta inexistente. - humanoContratos por confirmar: SAP ECC movimiento de activo, ingesta y
lastEventIdde Data Mesh, PATCH del Portal, cierre de orden de Bringoz (CSB-01); ficha y callback de la tienda, catálogo de estados SAP (CSB-02); estado de la entrega y los dos callbacks de entregas, más el catálogo dekindFlagy elprogramNamedel catálogo de programas de Bringoz (CSB-05, CSB-06); PUT Bringoz e INSERT mantenimiento del checkout; mensaje OrderPayed. DM-02 ya no está pendiente: EDM entregó ruta, request y response el 19 sep (CSB-07); queda coordinar el valor real del Bearer de dev y la prueba conjunta. AE-137: firma del equipo SAP y siembra de secretos BTP. - humanoNaming de adapters (Hugo, 26 ago): rutas
/erp/…,/crm/…,/delivery/…en inglés REST; secretos y Artifact Registry con prefijo del adapter. - resueltoVariables de entorno de los workflows. Cloud Workflows solo expone las suyas (
GOOGLE_CLOUD_PROJECT_ID,GOOGLE_CLOUD_OPERATION_ID…): el generador pediaGOOGLE_CLOUD_PROJECT(nombre de Cloud Run) en eltopicde cadapublishy en elsecret_idde cada credencial, yAPI_URLen los pasos de memoria compartida y mTLS. Ambas llegaban como null y tumbaban la ejecucion conTypeError: unsupported operand types for +. Corregido: la primera al nombre real, yAPI_URLviaja comouser_env_varsdel workflow que creadeployFlow, conapi_custom_audiencesen el Cloud Run para que acepte el OIDC que entra por el balanceador. - pipelineGates apagados en dev-fast (audit, tests, openapi-lint, schema-validation, scans): re-habilitar en qa/prod. jest de api requiere
--forceExit. - jiraÉpica "Gobernanza y operación de flujos" (rollback de versiones, edición concurrente, credenciales por entorno, reanudación tras fallos, exclusión mutua, headers/certificados) redactada en
docs/jira/, pendiente de crear en el tablero. - hechoAE-36 20/20: infra DEV, contratos Avro/OpenAPI con validación en CI, editor visual con deployFlow real, consumers de coreografía, LB + Cloud Armor + DNS, pipelines Bebbia, CSB-01, CSB-02, memoria compartida entre pasos, adapters migrados a repos propios y conectados por remote state, documentación de adapters publicada.
Referencias
| Recurso | Dónde |
|---|---|
| Arquitectura de solución (fuente de verdad) | docs/CSB-Arquitectura-Solucion-v1.1.0.pdf · docs/adr/ADR-001-sap-btp.md |
| Instalación y promoción | INSTALL.md · docs/PLAYBOOK-PROMOCION-FLUJOS.md · scripts/bootstrap-secrets.sh |
| Operación | docs/OPERACION-FALLOS-WORKFLOW.md · docs/MANUAL-FLOW-WALKTHROUGH.md · docs/DEMO-GUIA-CONEXION-OPEN-METEO.md |
| Contratos | docs/contracts/README.md · docs/contracts/csb-api.openapi.yaml · docs/contracts/AVRO-EVOLUTION.md · packages/shared/src/contracts · schemas/*.avsc |
| Seguridad | docs/security/IAM-ROLE-MATRIX.md · módulos csb-armor, csb-secrets, csb-kms |
| Adapters | ERP · CRM · Delivery — cada uno con Swagger, guía de despliegue e "Integración con el CSB" |
| Repos | rtp-csb · rtp-erp-adapter · rtp-crm-adapter · rtp-delivery-adapter |
| Documentos del repo | README.md, CLAUDE.md, SOUL.md, NEXT_STEPS.md, memory/, docs/README.md |