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.

NestJS 10 · Next.js 16 · Postgres 16 Pub/Sub · Workflows · Tasks · Eventarc Cloud Run × 5 + 3 adapters AE-36 cerrada 20/20 env: — mock GCP: —

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 activa dev
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

ComponenteQué haceDónde viveURL (dev)
csb-apiControl 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-webNext.js 16 + React Flow: editor visual (saga builder), registry, monitoring, arquitectura y contratos, login Googleapps/web · Cloud Run csb-webhttps://csb-dev.rotoplas.com → /flows, /registry/*, /monitoring/*, /architecture, /contracts, /settings/secrets
csb-audit-consumerPull listener de inventory.stock.updated.v1.audit.sub → tabla audit_log en el mismo Cloud SQL Postgresapps/audit-consumer · Cloud Run (min 1)interno
csb-analytics-consumerPull → BigQuery csb_analytics.csb_events (particionada por día, insertId = event_id)apps/analytics-consumer · Cloud Runinterno
csb-notification-consumerPull → dispara send-notification-workflow cuando quantity_after < reorder_pointapps/notification-consumer · Cloud Runinterno
AdaptersERP (SAP CPI + OData), CRM (HubSpot), Delivery (Bringoz/Nexus). Repos y Terraform propios; el CSB lee sus outputs por remote statertp-erp-adapter, rtp-crm-adapter, rtp-delivery-adapter/adapters/erp/docs · /adapters/crm/docs · /adapters/delivery/docs
packages/sharedContratos compartidos: pubsub.contracts.ts, cloud-tasks.contracts.ts, workflows.contracts.ts, csb-api.contracts.tspackages/shared/src/contracts—

Endpoints principales de csb-api

ÁreaRutasAuth
HealthGET /health (incluye env y version) · /health/readypública
AuthPOST /auth/login · GET /auth/google, /auth/google/callbackpública
Registry/topics, /schemas (+ /:id/activate), /api-registry (+ test-connection), /connectors, /adapters (+ /:id/probe), /secretsAPI key / JWT + roles
Flujos/flows, /flows/:id/graph, /flows/:id/publish, /flows/:id/versions, /flows/:id/preview, /flows/:id/validate, /flows/presenceAPI key / JWT + roles
ProvisioningPOST /provisioning/flows/:id/deploy · /execute · /redeploy/:deploymentId · GET /preview, /history, /execution-lockAPI key / JWT + roles
WebHook de flujoPOST /flows/:id/webhookHMAC-SHA256 X-CSB-Signature
Monitoring/monitoring/pubsub, /tasks, /dlq, /dlq/:topicName, POST /dlq/replay, /workflows/executions, /workflows/:name/sync, /flows/:id/live, /flow-memory, /dashboardAPI key / JWT
Bindings por ambiente/api-definitions/:id/bindings[/:env[/rotate]] · /flows/:flowId/bindings-statusAPI 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

PuertaQuién la usaAutenticaciónQué dispara
Topic Pub/Sub con schemaProductores 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 flujo
POST /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 topicHMAC-SHA256 del cuerpo crudo con csb-<env>-webhooks-signing-key, header X-CSB-SignatureexecuteFlow(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 manual
POST /api/provisioning/flows/{id}/execute
Personas desde el editor (botón Ejecutar) o scripts con JWTLogin CSB (usuario seed o Google OAuth), rolesMisma ruta que el WebHook, triggeredBy: 'manual'
Cloud Scheduler (nodo scheduler)Flujos periódicosOIDC del job hacia Workflows Executions APIdeployFlow crea el job con la expresión cron del nodo
Disponibilidad por centros
POST /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 pruebasAPI key / JWTPublica 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.

TopicSchema · orderingProductorConsumidores / disparaOrigen
inventory.stock.updated.v1Avro · entity_id (SKU) · DLQServicio de inventarioinventory-to-sap-saga (Eventarc); subs audit, analytics, notification, sap-adapter (push legado)Terraform
order.placed.v1Avro · order_id · DLQÓrdenes / tiendaorder-to-hubspot-saga; sub push hubspot-adapter (legado)Terraform
order.cancelled.v1AvroÓrdenesorder-cancellation-notifyTerraform
order.fulfillment.requested.v1JSON Schema · order_id · DLQSimulador / tiendaorder-fulfillment-orchestrator, order-fulfillment-demoTerraform + seed
sap.inventory.synced.v1 · sap.inventory.sync.failed.v1Avro · ordering por SKUERP adapter y sagaSin suscripción declarada (brecha)Terraform (adapter) + seed
hubspot.order.synced.v1JSON 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.v1JSON Schema · suscripcion_id · DLQTienda / backend BebbiaUn flujo por WS: bebbia-crear-cliente-sap, bebbia-modificar-cliente-sap, bebbia-anticipos-sap, bebbia-cancelar-plan-sap, bebbia-consultas-sap, bebbia-confirmar-instalacion-sapseed §12
sap.cliente.synced.v1, sap.anticipo.registered.v1, sap.plan.cancelled.v1, sap.consulta.completed.v1, sap.instalacion.confirmed.v1, sap.cpi.error.v1JSONFlujos Bebbia → SAP CPIaudit / notification (grafo)seed §12
bebbia.order.paid.v1JSON Schema (order_number, store_key, payment_id) · DLQCommerceTools (OrderPaymentAdded)crear-orden-trabajo-bebbiaseed §13
bebbia.order.validated.v1 → bebbia.work.order.created.v1, bebbia.first.charge.registered.v1JSONOrquestadores encadenadosfan-out logística + finanzas → apunte en Fluentseed §13
bebbia.installation.times.fetched.v1, bebbia.installation.times.assigned.v1, bebbia.installation.dispatch.failed.v1JSONobtener-horarios, asignar-horarioBackend Bebbia (consumer), reversión de agendaseed §13
bebbia.purifier.order.requested.v1 → assigned.v1 / rejected.v1JSON Schema · order_id · DLQTienda Bebbiapedido-purificador-instalacion → field ops consumerseed
inventory.asset.status.changed.v1 → validated.v1 / rejected.v1 → sap|datamesh|portal.updated.v1 / update.failed.v1JSONPortal de Inventario; Bringoz por WebHookCSB-01: validar-cambio-status-activo → fan-out a SAP ECC, Data Mesh, Portalseed (CSB-01)
store.customer.signup.completed.v1 / failed.v1 → notified.v1 / notify.failed.v1JSONCSB-02 recibir-alta-clientenotificar-alta-tienda → audit / notificationseed (CSB-02)
delivery.order.accepted.v1 / rejected.v1JSONprueba-entrega-bringoz, mantenimiento-bringozaudit / notification. El rechazo lleva la etapa donde se cortó (PETICION, TIPO_SERVICIO, ADAPTER, PROVEEDOR)seed (delivery)
erp.order.status.retrieved.v1 / failed.v1JSONprueba-estatus-pedido-erpaudit / 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.v1JSONcotizar-horarios-bringoz, releer-cotizacion-bringozpending dispara releer-cotizacion-bringoz (Eventarc, lleva el quoteId); los otros dos van a audit / notificationseed (delivery)
delivery.timeslot.assigned.v1 / rejected.v1JSONasignar-horario-bringozaudit / notification. El rechazo del proveedor significa recotizar; no se reencadena soloseed (delivery)
store.delivery.order.created.v1 / failed.v1 → notified.v1 / notify.failed.v1JSONCSB-05 recibir-orden-entreganotificar-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.v1JSONCSB-06 cotizar-slots-entreganotificar-slots-tienda → audit / notification. pending es terminal aquí: lleva el quoteId con el contrato de releer-cotizacion-bringoz y lo recoge CSB-09seed (CSB-06)
csb.health.ping.v1 · csb.hello.world.v1Avro · JSONCloud Scheduler · hello-world-flowHealthcheck del bus (csb-healthcheck.sh) · validación mínimaTerraform · 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)

WorkflowDisparadorPasosResultadoEstado
inventory-to-sap-sagainventory.stock.updated.v1 (Eventarc) o Cloud Tasksvalidar → 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-syncroto el ERP adapter no expone /transform ni status
compensate-inventory-syncInvocado por la sagaGET /sync/{id}/status → rollback si PARTIAL → notificar operaciones—roto rutas inexistentes
order-to-hubspot-sagaorder.placed.v1validar → /contacts/resolve → /deals/transform → /deals/syncretorna { status, hubspotDealId }; en fallo DLQroto el CRM adapter solo tiene /sync
order-cancellation-notifyorder.cancelled.v1/deals/sync con envelope <id>-cancel—roto
order-fulfillment-orchestratororder.fulfillment.requested.v1reserva SAP (/sync, compensación RELEASE_RESERVATION) → contacto → deal (compensación CANCEL_DEAL)DLQ propia en falloroto contratos de /sync no coinciden
send-notification-workflowcsb-notification-consumernotificación de reorden (log-only en dev)—ok

Flujos del editor (seed)

FlujoDisparadorQué haceSistemasVerificación
crear-orden-trabajo-bebbia (CreateWorkOrder v176)bebbia.order.paid.v14 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-fluentFluent, CommerceTools, Bringoz, Apigee, CF, Add-on, SAP CPI7 escenarios SUCCEEDED (2 sep) contra mocks
obtener-horarios-instalacion (GetTimes v100)API triggertoken Bringoz → depots → opciones de horario de {idFluent}-{Kindflag}Bringozok (mock)
asignar-horario-instalacion (AssignTimes v32)API triggerasignar-horario (mantenimiento status 9, reserva de equipo, dispatch con compensación, warehouse en Fluent, slot, correo, motivo) → si falla revertir-agenda-y-reconsultar-horariosCF, Bringoz, Fluent, Backend, SendGridok (mock)
pedido-purificador-instalacionbebbia.purifier.order.requested.v1disponibilidad on-hand en Fluent → orden + reserva + instalador y alta WS1; sin stock publica rejectedFluent, SAP CPIok (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, Portal7 escenarios SUCCEEDED (2 sep); contratos reales pendientes
alta-cliente-tienda (CSB-02)WebHook firmado de la tiendarecibir-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, CSBRevisión Bebbia 9 sep atendida (registro propio, 409 por customerKey, 202 acuse, config del servidor); callback y API key pendientes de Bebbia
prueba-entrega-bringozEjecució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 BringozDelivery adapter (rtp-delivery-adapter); Bringoz sólo detrás de éldelivery.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-erpEjecució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 repetirERP adapter (rtp-erp-adapter); SAP CPI sólo detrás de élerp.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 éldelivery.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 listaDelivery adapter; Bringoz sólo detrás de éldelivery.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.v1GET {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 eternoDelivery adapter; Bringoz sólo detrás de éldelivery.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 cuerpoDelivery adapter; Bringoz sólo detrás de éldelivery.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 tiendaTienda Bebbia (x-rotoplas-api-key), Delivery adapter; Bringoz sólo detrás de élstore.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 élstore.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 editorUna 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 lecturaCore 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.*.v1Mapeo crudo de cada WS CPI (WS1, WS2, WS3, WS5, WS7, WS11) → topic sap.*.v1 o sap.cpi.error.v1SAP CPI gatewayok (mock)
inventory-to-sap-saga, order-to-hubspot-saga, order-cancellation-notify (grafos ACTIVE)ver arribaRepresentación en el editor de los sagas Terraform, con nodo adapter csb-sap-adapter / csb-hubspot-adapteradaptersver estado arriba
order-fulfillment-demo · hello-world-flowmanual · csb.hello.world.v1Demo 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/healthadapters · csb-apidemo
Cómo leer un flujo En /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)

AdapterCloud Run · cola · topicCómo lo alcanza el CSBIdentidadDocumentación
ERP SAP CPI + ODatacsb-erp-adapter · csb-erp-adapter-queue · sap.inventory.synced.v1remote state csb-erp-adapter → local.adapters.erp → ERP_ADAPTER_URL; Cloud Tasks (SAP_ADAPTER_QUEUE) o http.post OIDCcsb-api actúa como csb-erp-adapter-tasks-sa/adapters/erp/docs
CRM HubSpotrtp-transversal-dev-dev-crm-adapter · csb-hubspot-adapter-queue-dev · csb-crm-results-devremote state crm-adapter/dev → CRM_ADAPTER_URL; Cloud Tasks (HUBSPOT_ADAPTER_QUEUE) o OIDCel adapter otorga run.invoker a csb-run-sa (no coincide con sa-csb-api-dev)/adapters/crm/docs
Delivery Bringoz / Nexuscsb-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)

ApiDefinitionAuth · secreto (csb-<env>-…)Usada porBase URL (variable)
Fluent Commerce OMSBASIC → OAuth2 password grant · fluent-credentialcrear-orden-trabajo, asignar-horario, purificadorFLUENT_API_URL
CommerceTools Orders APIBASIC client_credentials · commercetools-credentialvalidar-ordenCOMMERCETOOLS_API_URL / _AUTH_URL / _CLIENT_ID
Bringoz Logistics v2API key x-api-key → token · bringoz-credentiallogística, horarios, dispatch, CSB-01BRINGOZ_API_URL
Apigee Gateway Bebbia · Bebbia Cloud Functions · Bebbia Backend · SendGrid CF · Cancel Reason CF · SAP First Charge Logger · Bebbia Add-on APIBASIC / NONE / API key · apigee-credential, bebbia-addon-credentialPipelines BebbiaAPIGEE_API_URL, BEBBIA_*_URL
SAP CPI bebbia GatewayBASIC · sap-cpi-user + sap-cpi-passwordbebbia-*-sap, CSB-02, purificador, WS3 del checkoutSAP_CPI_BASE_URL
Portal de Inventario Rotoplas · Core Data Mesh Rotoplas · SAP ECC InventarioAPI key · BEARER · BASIC · portal-inventario, data-mesh, sap-eccCSB-01INVENTORY_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-bebbiaCSB-02BEBBIA_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 nuevoCSB-05, CSB-06BEBBIA_STORE_API_URL
CSB Registro de altas SAP (/internal/customer-signups)OIDC (el propio workflow)CSB-02 candado de idempotenciaAPI_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 adapterprueba-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 directoDELIVERY_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 adapterEstatus 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 APIOAuth2 / BEARER · sap-api-credentials, hubspot-api-tokenSagas 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

ConsumerSuscripciónDestinoIdempotencia
csb-audit-consumerinventory.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-consumerinventory.stock.updated.v1.analytics.sub (pull)BigQuery csb_analytics.csb_events, particionada por received_atQuery por event_id + insertId = event_id
csb-notification-consumerinventory.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, workflow
  • workflow → adapter, topic, external-api
  • adapter → external-api, topic
  • external-api → adapter · consumer es 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 o auth: OIDC, result, resultFilter, retryPolicy, pathParamValues)
  • try = call + compensationCall
  • condition (sintaxis Cloud Workflows: and/or/not) con trueNextId/falseNextId; end termina con éxito
  • publish (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

  1. Guardar — PUT /flows/:id/graph
  2. Preview GCP — GET /provisioning/flows/:id/preview: qué se crea, qué se salta (managed_by=terraform), costo estimado
  3. Deploy — POST …/deploy: topics, DLQs, suscripciones (push a <adapter>/events en edges topic→adapter), colas que-<flow>-<adapter>, workflows compilados a YAML, triggers Eventarc, jobs Scheduler; snapshot inmutable con version
  4. Ejecutar — POST …/execute; una sola ejecución activa por flujo (409)
  5. Historial / redeploy — GET …/history, POST …/redeploy/:deploymentId crea una versión nueva (rollback v3→v1 crea v4)

Guardas

  • Anti-colisión: nunca sobrescribe un recurso con managed_by=terraform; colas con prefijo que- como señal
  • Bindings por ambiente: el grafo guarda logicalKey; el deploy falla rápido si falta el binding del CSB_ENV
  • Edición concurrente: presencia y 409 en vez de last-write-wins
  • MOCK_GCP=true en 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 .avsc en schemas/, registrados en Pub/Sub Schemas; evolución FULL_COMPATIBLE (docs/contracts/AVRO-EVOLUTION.md) validada en CI.
  • API control plane: OpenAPI 3.1 exportado con npm run openapi:export a docs/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, headers X-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): retryPolicy del paso; default maxAttempts 3, 1 s → 8 s ×2 con http.default_retry_predicate. Nunca se reintenta la compensación.
  • Error de negocio (4xx) o retries agotados: compensationCall si el paso es try; la ejecución queda FAILED. 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 por event_id o clave de negocio).
  • Mensajes irrecuperables van a la DLQ del topic; se reprocesan con POST /api/monitoring/dlq/replay o scripts/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.app responde 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-sa de 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 key x-api-key para integraciones; rutas internas de Workflows con OIDC (OidcAuthGuard). La api key válida es el secreto csb-<env>-internal-api-key, que Cloud Run inyecta como API_KEYS e INTERNAL_API_KEY: ApiKeyGuard lee las dos y falla cerrado — si no hay ninguna clave configurada rechaza, no acepta. Quien entra por api key no lleva rol y RolesGuard lo 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 por scripts/bootstrap-secrets.sh.
  • KMS (csb-<env>-api-credentials) para el DEK que envuelve credenciales guardadas en ApiDefinition (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óduloRecursos
csb-computeCloud 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-workflowTopics 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-prerequisitesCloud 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-policyVPC 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)

  1. install · lintModo dev-fast: security-audit, test (jest de api se cuelga en teardown), openapi-lint, schema-validation y scan-* comentados; re-habilitar en qa/prod.
  2. build-api · build-web · build-{audit,analytics,notification}-consumerImágenes en paralelo, etiquetadas con $SHORT_SHA; push a Artifact Registry csb-adapters.
  3. tf-init → tf-import → tf-plan → tf-applyEn infra/envs/$_ENV; importa secretos preexistentes; la imagen se ignora (lifecycle.ignore_changes).
  4. deploy-* con --no-trafficCinco servicios; startup probe de csb-api ampliado (~3 min).
  5. smoke-testscripts/csb-smoke-test.sh.
  6. traffic-* --to-latestSolo si el smoke test pasa.
  7. 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.
Este portal Los HTML viven en 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, /connectors en 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 con GOOGLE_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 con PUT /api/api-definitions/:id/bindings/:env y cargar el secreto con /rotate.
  • Si el YAML apunta a URLs de DEV en otro ambiente: falta baseUrlOverride en 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 deploy desplegando la misma imagen provocaba doble revisión: hoy Terraform ignora la imagen.
  • csb-api necesita 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.

  1. Verificar bindings del ambiente destinoGET /api/flows/:flowId/bindings-status; crear los faltantes con PUT /api/api-definitions/:id/bindings/:env (secretRef, baseUrlOverride).
  2. Cargar los secretosbootstrap-secrets.sh o POST …/bindings/:env/rotate; nombres csb-<env>-*.
  3. Desplegar en el ambiente destinoTerraform en infra/envs/<env> con CSB_ENV correcto; deploy de los flujos desde el editor.
  4. VerificarHealth de los cinco servicios, ejecución de prueba de cada flujo, backlog cero en DLQs. Checklist completo en docs/PLAYBOOK-PROMOCION-FLUJOS.md e INSTALL.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 a main; nunca prod sin confirmación humana.
  • Conventional Commits; todo recurso GCP en Terraform o deployFlow con 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-workflow no declara user_env_vars (SAP_ADAPTER_URL, HUBSPOT_ADAPTER_URL quedan vacías), las SAs de los workflows no tienen run.invoker en 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íncrono POST /<dominio>/<acción> con SyncPayloadDto) y aplicarlo.
  • legadoAdapters internos en csb-compute. sap-adapter y hubspot-adapter siguen declarados con suscripciones push (inventory.stock.updated.v1, order.placed.v1); retirarlos y apuntar a local.adapters.*.
  • deudaWeb services Bebbia directos a CPI. WS1 ya migró: CSB-02 llama POST {{ERP}}/erp/customers en el ERP adapter con OIDC (API CSB ERP Adapter, base ERP_ADAPTER_URL), así que el host del gateway y el secreto sap-cpi-token salieron de ese flujo. Faltan WS3 (CSB-03 primer cargo), WS11 (CSB-04 instalación) y los mapeos crudos de seed.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, ApiDefinition CSB 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 de bebbia-pipelines.ts siguen llamando a {{BRINGOZ}} (BRINGOZ_API_URL, sin valor en infra/: en dev apuntan al mock inexistente mock-bringoz-api). Migrar esos pasos a {{DELIVERY}} con auth: 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.v1 y csb-crm-results-dev no tienen suscripción del CSB; hubspot.order.synced.v1 nadie lo publica; edges topic→adapter provisionan push a /events, ruta inexistente.
  • humanoContratos por confirmar: SAP ECC movimiento de activo, ingesta y lastEventId de 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 de kindFlag y el programName del 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 pedia GOOGLE_CLOUD_PROJECT (nombre de Cloud Run) en el topic de cada publish y en el secret_id de cada credencial, y API_URL en los pasos de memoria compartida y mTLS. Ambas llegaban como null y tumbaban la ejecucion con TypeError: unsupported operand types for +. Corregido: la primera al nombre real, y API_URL viaja como user_env_vars del workflow que crea deployFlow, con api_custom_audiences en 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

RecursoDó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ónINSTALL.md · docs/PLAYBOOK-PROMOCION-FLUJOS.md · scripts/bootstrap-secrets.sh
Operacióndocs/OPERACION-FALLOS-WORKFLOW.md · docs/MANUAL-FLOW-WALKTHROUGH.md · docs/DEMO-GUIA-CONEXION-OPEN-METEO.md
Contratosdocs/contracts/README.md · docs/contracts/csb-api.openapi.yaml · docs/contracts/AVRO-EVOLUTION.md · packages/shared/src/contracts · schemas/*.avsc
Seguridaddocs/security/IAM-ROLE-MATRIX.md · módulos csb-armor, csb-secrets, csb-kms
AdaptersERP · CRM · Delivery — cada uno con Swagger, guía de despliegue e "Integración con el CSB"
Reposrtp-csb · rtp-erp-adapter · rtp-crm-adapter · rtp-delivery-adapter
Documentos del repoREADME.md, CLAUDE.md, SOUL.md, NEXT_STEPS.md, memory/, docs/README.md