00

Como encaja el adapter

GCP · rtp-transversal-dev · us-central1 IAM POST HTTPS Publica resultado Suscripcion push rtp-csb Orquestador Cloud Run Produce tareas Consume resultados Cloud Tasks HTTP POST · OIDC csb-crm-adapter NestJS 11 · Puerto 8080 · Cloud Run CONTROLLERS SyncController POST /sync HealthController GET /health(*) SERVICIOS SyncService Transformer CLIENTE EXTERNO HubSpotClient BEARER TOKEN upsert · search · patch ResultPublisher UPSERT POST create → 409? → search email → PATCH HubSpot CRM REST API v3 api.hubapi.com POST /contacts (create) POST /contacts/search PATCH /contacts/{id} GET /contacts?limit=1 (ping) Bearer token via Secret Manager Secret Manager csb-dev-hubspot-api-token Cloud Pub/Sub Topic: csb-crm-results · CloudEvents v1.0 Nota · El adapter nunca se expone a internet · entrada solo via Cloud Tasks (IAM) · upsert create→409→search→PATCH · resultados via Pub/Sub
01

Anatomia interna del adapter

Modulos NestJS

Modulo Controllers / Providers Responsabilidad
AppModule Importa HealthModule, SyncModule Root module — bootstrap
SyncModule SyncController, SyncService, HubSpotTransformer, HubSpotClientService, ResultPublisherService Flujo completo de sincronizacion de contactos
HealthModule HealthController, HubSpotClientService Liveness + readiness probes, ping a HubSpot + Secret Manager

Flujo de request — POST /sync

Cloud Tasks
→
SyncController
valida DTO
→
SyncService
orquesta
→
Transformer
envelope → props
→
HubSpotClient
upsert contact
→
ResultPublisher
Pub/Sub

Logica de upsert del HubSpotClient

El client intenta crear el contacto con POST /crm/v3/objects/contacts. Si HubSpot responde 409 Conflict (el email ya existe), busca el contacto por email con POST /contacts/search y lo actualiza con PATCH /contacts/{id}.

HubSpotClientService — upsert flow
// 1. Intento de creacion
const response = await fetch(`${baseUrl}/crm/v3/objects/contacts`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
  body: JSON.stringify(data),
});

// 2. Si 409 → buscar por email, luego PATCH
if (response.status === 409) {
  return this.updateByEmail(data, token);
  // POST /contacts/search → filterGroups email=EQ
  // PATCH /contacts/{id}  → actualiza propiedades
}

Propiedades mapeadas por el Transformer

Propiedad HubSpot Campo del envelope.data Notas
email email Requerido — llave de deduplicacion
firstname first_name —
lastname last_name —
phone phone —
company company —
csb_event_id envelope.id Trazabilidad hacia el evento original
csb_source envelope.source Origen del evento
csb_synced_at (generado) Timestamp ISO de sincronizacion
!
Las propiedades csb_event_id, csb_source y csb_synced_at son propiedades custom que deben existir en HubSpot antes del primer sync. Crearlas en Settings → Properties → Contact properties como tipo Single-line text.
02

Conectar el CSB con el adapter

1
Registrar el endpoint en el seed del CSB
En la configuracion de destinations del CSB, agrega la URL del Cloud Run como destino de Cloud Tasks.
rtp-csb — seed / config de destinations
// URL de Cloud Run (una vez desplegado)
const CRM_ADAPTER_URL = 'https://rtp-transversal-dev-dev-crm-adapter-XXXX-uc.a.run.app';

// Rutas que Cloud Tasks debe enviar al adapter
const destinations = {
  contact_sync: `${CRM_ADAPTER_URL}/sync`,
  // Endpoints planeados (ver seccion 07):
  // contact_resolve: `${CRM_ADAPTER_URL}/contacts/resolve`,
  // deals_transform: `${CRM_ADAPTER_URL}/deals/transform`,
  // deals_sync:      `${CRM_ADAPTER_URL}/deals/sync`,
};
2
Crear la Cloud Tasks queue apuntando al adapter
La queue necesita la service account del adapter como oidcToken.serviceAccountEmail para que Cloud Tasks pueda llamar al Cloud Run con IAM. La queue se crea via Terraform (modulo crm-compute), pero si necesitas crearla manualmente antes del primer deploy:
gcloud — crear queue
gcloud tasks queues create csb-hubspot-adapter-queue-dev \
  --location=us-central1 \
  --max-concurrent-dispatches=5 \
  --max-attempts=5 \
  --min-backoff=10s \
  --max-backoff=300s \
  --project=rtp-transversal-dev
3
Formato del payload que el CSB envia al adapter
El CSB construye un SyncPayloadDto (CloudEvents v1.0 + metadata de tarea) y lo pone como body del HTTP POST que Cloud Tasks entrega al adapter.
POST /sync — SyncPayloadDto (contacto HubSpot)
{
  "envelope": {
    "id":            "evt-abc123",           // UUID del evento original
    "source":        "//bebbia/orders-service",
    "type":          "com.bebbia.order.placed.v1",
    "specversion":   "1.0",
    "entityid":      "ORD-2026-0042",
    "data": {
      "email":      "maria@example.com",    // REQUERIDO — llave upsert
      "first_name": "Maria",
      "last_name":  "Lopez Garcia",
      "phone":      "5512345678",
      "company":    "Bebbia Enterprise"
    }
  },
  "taskMetadata": {
    "retryCount": 0,
    "taskName":   "projects/rtp-transversal-dev/locations/us-central1/queues/csb-hubspot-adapter-queue-dev/tasks/TASK-ID",
    "queueName":  "csb-hubspot-adapter-queue-dev"
  }
}
!
El campo envelope.data.email es obligatorio. Si no viene, el Transformer lanza Error: Missing required field: email y Cloud Tasks reintentara segun la configuracion de la cola.
4
Resultado que el adapter publica al CSB via Pub/Sub
Tras sincronizar con HubSpot, el adapter publica un CloudEvent v1.0 en el topic configurado. El CSB lo consume con una suscripcion push.
Pub/Sub — CloudEvent publicado por el adapter
{
  "specversion":     "1.0",
  "type":            "hubspot.contact.synced.v1",
  "source":          "//csb-crm-adapter/rtp-transversal-dev",
  "id":              "evt-abc123-result",    // originalEventId + "-result"
  "time":            "2026-08-27T20:00:01.000Z",
  "datacontenttype": "application/json",
  "data": {
    "originalEventId": "evt-abc123",       // trazabilidad al evento CSB
    "email":           "maria@example.com",
    "hubspotId":       "12345678",         // ID del contacto en HubSpot
    "action":          "created",           // "created" | "updated"
    "status":          "SUCCESS"
  }
}
// attributes del mensaje Pub/Sub:
// ce_type     = hubspot.contact.synced.v1
// ce_source   = //csb-crm-adapter/rtp-transversal-dev
// ce_specversion = 1.0
5
Suscripcion del CSB al topic de resultados
El CSB necesita una suscripcion al topic csb-crm-results-dev. Terraform crea el topic y una suscripcion base, pero si el CSB necesita una suscripcion push apuntando a su propio endpoint:
gcloud — crear suscripcion push en CSB
gcloud pubsub subscriptions create csb-crm-results-csb-push \
  --topic=csb-crm-results-dev \
  --push-endpoint=https://<CSB_URL>/events/hubspot-sync \
  --ack-deadline=60 \
  --project=rtp-transversal-dev
03

Prerequisitos antes del primer deploy

Secrets en Secret Manager

El adapter usa un solo secret para autenticarse con HubSpot. El patron es csb-{env}-{name}:

Secret Descripcion Donde obtenerlo
csb-dev-hubspot-api-token Private App token de HubSpot HubSpot → Settings → Integrations → Private Apps
gcloud — crear el secret
# Crear el secret (reemplaza con el token real de la Private App)
echo -n "pat-na1-XXXXX-XXXX-XXXX" | \
  gcloud secrets create csb-dev-hubspot-api-token \
    --data-file=- \
    --project=rtp-transversal-dev

# Verificar que se creo correctamente
gcloud secrets versions access latest \
  --secret=csb-dev-hubspot-api-token \
  --project=rtp-transversal-dev
!
La Private App en HubSpot necesita los siguientes scopes: crm.objects.contacts.read, crm.objects.contacts.write. Para endpoints futuros (deals): crm.objects.deals.read, crm.objects.deals.write.

Variables de entorno

Si USE_SECRET_MANAGER=false (desarrollo local), el adapter lee directamente de env vars. En Cloud Run, Terraform configura USE_SECRET_MANAGER=true automaticamente.

Variable Default Descripcion
USE_SECRET_MANAGER false Si true, lee token de Secret Manager
HUBSPOT_API_TOKEN (vacio) Token directo (solo si USE_SECRET_MANAGER=false)
HUBSPOT_BASE_URL https://api.hubapi.com URL base de HubSpot API
GOOGLE_CLOUD_PROJECT (vacio) Proyecto GCP
GCP_PROJECT_ID rtp-transversal-dev Alias del proyecto (usado por ResultPublisher)
PUBSUB_RESULT_TOPIC csb-crm-results Topic de Pub/Sub para resultados
RESULT_TOPIC hubspot.contact.synced.v1 Tipo del CloudEvent (config)
NODE_ENV dev Ambiente (dev/qa/production)
PORT 8080 Puerto del servidor

Permisos IAM minimos

Service Account Rol Para que
csb-crm-adapter-dev roles/secretmanager.secretAccessor Leer el token de HubSpot en runtime
csb-crm-adapter-dev roles/pubsub.publisher Publicar resultados de sync
csb-run-sa roles/run.invoker Cloud Tasks (via CSB) puede llamar al Cloud Run

Propiedades custom en HubSpot

Antes del primer sync, crea estas propiedades en HubSpot → Settings → Properties → Contact:

Internal name Label Type
csb_event_id CSB Event ID Single-line text
csb_source CSB Source Single-line text
csb_synced_at CSB Synced At Single-line text
04

Deploy manual a Cloud Run (primer deploy)

Para el primer deploy antes de tener el pipeline CI/CD conectado al trigger.

1
Build y push de la imagen
shell
gcloud auth configure-docker us-central1-docker.pkg.dev

IMAGE=us-central1-docker.pkg.dev/rtp-transversal-dev/csb-adapters/crm-adapter:latest

docker build -t $IMAGE .
docker push $IMAGE
2
Deploy a Cloud Run con variables de entorno
gcloud — deploy
gcloud run deploy rtp-transversal-dev-dev-crm-adapter \
  --image=$IMAGE \
  --region=us-central1 \
  --platform=managed \
  --no-allow-unauthenticated \
  --service-account=csb-crm-adapter-dev@rtp-transversal-dev.iam.gserviceaccount.com \
  --set-env-vars="NODE_ENV=dev,\
USE_SECRET_MANAGER=true,\
GOOGLE_CLOUD_PROJECT=rtp-transversal-dev,\
GCP_PROJECT_ID=rtp-transversal-dev,\
PUBSUB_RESULT_TOPIC=csb-crm-results-dev" \
  --port=8080 \
  --min-instances=0 \
  --max-instances=3 \
  --memory=512Mi \
  --project=rtp-transversal-dev
3
Verificar health check
shell — health check con identidad GCP
URL=$(gcloud run services describe rtp-transversal-dev-dev-crm-adapter \
  --region=us-central1 --format='value(status.url)')

TOKEN=$(gcloud auth print-identity-token)

curl -H "Authorization: Bearer $TOKEN" $URL/health | jq .

# Respuesta esperada:
# {
#   "status": "ok",
#   "timestamp": "2026-08-27T...",
#   "version": "0.1.0",
#   "component": "csb-crm-adapter",
#   "checks": {
#     "hubspot_connectivity": "ok",
#     "secret_manager": "ok"
#   }
# }
05

Terraform — infraestructura listo

✓
El directorio infra/ esta creado con dos modulos y tres ambientes (dev, qa, prod). El pipeline de Cloud Build ejecuta terraform init → plan → apply automaticamente.

Estructura de infra/

arbol de directorios
infra/
  envs/
    dev/
      main.tf          # SA + modulos + outputs
      variables.tf     # project_id, region, image_tag, csb_invoker_sa_email
      backend.tf       # GCS backend
      terraform.tfvars # rtp-transversal-dev, us-central1
    qa/                # espejo de dev con tfvars de qa
    prod/              # espejo de dev con tfvars de prod
  modules/
    crm-compute/       # Cloud Run + Pub/Sub + Cloud Tasks + IAM invoker
    crm-secrets/       # Secret Manager secret + IAM secretAccessor

Recursos declarados

Modulo / Archivo Recurso Terraform Descripcion
crm-compute google_cloud_run_v2_service Cloud Run del adapter (ingress: INTERNAL_ONLY)
crm-compute google_pubsub_topic Topic csb-crm-results-{env} (retencion 7d)
crm-compute google_pubsub_subscription Suscripcion base para el CSB
crm-compute google_cloud_tasks_queue Cola csb-hubspot-adapter-queue-{env}
crm-compute google_cloud_run_v2_service_iam_member CSB SA como run.invoker
crm-secrets google_secret_manager_secret Secret csb-{env}-hubspot-api-token
crm-secrets google_secret_manager_secret_iam_member Adapter SA como secretAccessor
envs/dev/main.tf google_service_account SA csb-crm-adapter-{env}
envs/dev/main.tf google_project_iam_member SA con pubsub.publisher

Outputs de Terraform

Output Uso
service_url Registrar en CSB como CRM_ADAPTER_URL para Cloud Tasks
queue_name Nombre de la cola de Cloud Tasks
result_topic_id Topic de Pub/Sub — CSB consume resultados de aqui
crm_adapter_sa Email del Service Account del adapter
06

Pipeline CI/CD — cloudbuild.yaml listo

✓
cloudbuild.yaml esta en la raiz del repo con un pipeline de 7 pasos. Usa node:20-alpine para dependencias/lint/test y hashicorp/terraform:1.7 para infra. Cobertura minima: 65% en lineas y funciones.
1
Security audit + Lint + Tests (en paralelo)
Tres pasos que corren en paralelo (waitFor: ['-']): npm audit --audit-level=high, npm run lint, y npm run test:cov con threshold de 65%.
2
Docker build
Construye imagen con tags $SHORT_SHA y $_ENV-latest. Usa --cache-from para builds incrementales.
3
Docker push
Push con --all-tags a Artifact Registry (csb-adapters).
4
Terraform init → plan → apply
Ejecuta en infra/envs/$_ENV, pasa -var=image_tag=$SHORT_SHA y -var=csb_invoker_sa_email=$_CSB_INVOKER_SA al plan.
5
Deploy --no-traffic
Despliega nueva revision sin recibir trafico.
6
Smoke test via gcloud proxy
Usa gcloud run services proxy (porque ingress es INTERNAL_ONLY). Hasta 12 intentos cada 5s contra /health.
7
Promover trafico a latest
Solo si el smoke test pasa: gcloud run services update-traffic --to-latest.
cloudbuild.yaml — substitutions
substitutions:
  _REGION:  us-central1
  _REPO:    csb-adapters
  _SERVICE: crm-adapter
  _ENV:     dev
  _MIN_COVERAGE: "65"
  _CSB_INVOKER_SA: csb-run-sa@rtp-transversal-dev.iam.gserviceaccount.com

options:
  logging:     CLOUD_LOGGING_ONLY
  machineType: E2_HIGHCPU_8

timeout: 1800s
07

Smoke test E2E

Una vez desplegado, verifica el flujo completo:

CSB
→
Cloud Tasks
POST /sync
→
CRM Adapter
→
HubSpot CRM
→
Pub/Sub
hubspot.contact.synced.v1
→
CSB
curl — llamada directa al adapter (con IAM token)
URL=$(gcloud run services describe rtp-transversal-dev-dev-crm-adapter \
  --region=us-central1 --format='value(status.url)')
TOKEN=$(gcloud auth print-identity-token)

curl -X POST "$URL/sync" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "envelope": {
      "id": "test-smoke-001",
      "source": "//bebbia/smoke-test",
      "type": "com.bebbia.contact.test.v1",
      "specversion": "1.0",
      "entityid": "SMOKE-001",
      "data": {
        "email": "smoke-test@example.com",
        "first_name": "Smoke",
        "last_name": "Test",
        "phone": "5500000000",
        "company": "Bebbia QA"
      }
    },
    "taskMetadata": {
      "retryCount": 0,
      "taskName": "manual-smoke-test",
      "queueName": "manual"
    }
  }' | jq .

# Respuesta esperada:
# { "status": "ok" }

# Verificar en HubSpot:
# CRM → Contacts → buscar smoke-test@example.com
# Debe tener csb_event_id = "test-smoke-001"
08

Endpoints — actuales y planeados

Metodo + Ruta Descripcion Estado Referencia saga
POST /sync Upsert de contacto (create o update por email) listo order-to-hubspot-saga
GET /health Liveness probe — conectividad HubSpot + Secret Manager listo —
GET /health/ready Readiness probe listo —
POST /contacts/resolve Buscar contacto existente en HubSpot por email/ID planeado order-to-hubspot-saga
POST /deals/transform Transformar datos de negocio a propiedades de deal HubSpot planeado order-to-hubspot-saga
POST /deals/sync Crear/actualizar deal en HubSpot asociado a un contacto planeado order-to-hubspot-saga
!
Los endpoints /contacts/resolve, /deals/transform y /deals/sync estan referenciados por la saga order-to-hubspot-saga.yaml en el CSB. Deben implementarse antes de activar esa saga en produccion.
09

Orden de ejecucion recomendado

# Tarea Estado
1 Crear propiedades custom en HubSpot (csb_event_id, csb_source, csb_synced_at) pendiente
2 Crear Private App en HubSpot con scopes de contacts pendiente
3 Crear secret csb-dev-hubspot-api-token en Secret Manager pendiente
4 Crear directorio infra/ con Terraform listo
5 Crear cloudbuild.yaml listo
6 Crear Dockerfile (multi-stage, non-root) listo
7 Crear Artifact Registry repo csb-adapters (si no existe) pendiente
8 Deploy manual (seccion 04) para validar imagen pendiente
9 Registrar URL del Cloud Run en seed del CSB pendiente
10 Crear Cloud Tasks queue (via Terraform o manual) pendiente
11 Crear suscripcion Pub/Sub push en CSB pendiente
12 Smoke test E2E (seccion 07) pendiente
13 Implementar endpoints planeados (seccion 08) post-deploy
14 Eliminar apps/hubspot-adapter de rtp-csb post-migracion
✓
Codigo fuente, infra Terraform, Dockerfile y pipeline CI/CD listos en rama dev. Lo pendiente es configuracion GCP (secret del token HubSpot, registrar trigger de Cloud Build), configuracion en HubSpot (Private App + propiedades custom) y el primer deploy.