Skip to content

Contactos Cobranza Adela (Google Sheet → BigQuery)

Lleva el directorio de contactos de cobranza (un Google Sheet que mantiene CuSu a mano) a BigQuery, ya validado y normalizado, para que el agente de voz de cobranza de Adela (ElevenLabs) lo consuma vía una question de Metabase a través del backend de Lean.

Puente temporal — el destino era HubSpot

Este proceso existe porque hoy los contactos viven en un Sheet editado a mano. La fuente de verdad a futuro es HubSpot (ya replicado a solvento-data-prod.hubspot.* vía Airbyte). Cuando la migración esté, se reescribe solo el SELECT de la question de Metabase apuntando a HubSpot y este servicio se deprecia.

Actualización 2026-10: el CRM pasa de HubSpot a Lightfield, así que el destino hay que redefinirlo. Lightfield ya replica contacts a solvento-data-prod.lightfield.contacts (sin modelo dbt todavía) — ver Airbyte · Lightfield.

¿Llegaste hasta acá desde…?

Si ves… Eso sale de… La lógica está en…
Contactos que llama el agente de cobranza de Adela solvento-data-prod.elevenlabs.contactos_cobranza Cloud Run contactos-adela-sheets-etl (transform.py)
Un teléfono normalizado (+52…) para cobranza Columnas *_telefono_e164 de esa tabla transform.py (normalización E.164)
Por qué un contacto no se llama (is_contactable = false) Columnas is_contactable / estado_raw / data_warnings transform.py (flag de estado + hay teléfono de cobranza válido)
Un contacto que "desapareció" o no entró …_rejected (con reject_reason) transform.py (sin RFC / RFC duplicado)
Campo Valor
Origen Google Sheet "Contactos Cartera" (Drive, editado por CuSu)
Patrón Cloud Run agendado (pull del Sheet vía Sheets API → validación → carga a BigQuery)
Destino solvento-data-prod.elevenlabs.contactos_cobranza (+ _history, _rejected)
Frecuencia 3x por dia (Cloud Scheduler contactos-adela-sheets-etl-daily, 0 8,13,18 hora Mexico, POST /extract)
Criticidad 🟡 Media (alimenta un agente que llama clientes; volumen bajo, puente temporal)
Dueño Martin (data eng) · agente/negocio: María Sánchez · backend: Leandro Gutiérrez
Repos contactos-adela-sheets-etl · terraform-gcp-infrastructure

Diagrama de linaje

graph LR
    SCH[["⏰ Cloud Scheduler<br/>contactos-adela-sheets-etl-daily<br/>0 8,13,18 (MX) · POST /extract"]]
    SH[["📄 Google Sheet<br/>Contactos Cartera (CuSu)"]]
    CR["Cloud Run<br/>contactos-adela-sheets-etl<br/>(Flask · valida + normaliza)"]
    BQ[("solvento-data-prod.elevenlabs<br/>contactos_cobranza<br/>(+_history, +_rejected)")]
    MB["Question Metabase 6321<br/>(directorio + is_contactable)"]
    API["backend Lean<br/>(capa auth)"]
    EL["🤖 Agente ElevenLabs<br/>(cobranza)"]

    SCH -->|dispara| CR
    SH -->|Sheets API · read-only| CR --> BQ --> MB --> API --> EL

    classDef src fill:#fde68a,stroke:#b45309,color:#000;
    classDef store fill:#bfdbfe,stroke:#1e40af,color:#000;
    class SCH,SH src;
    class BQ store;

Paso a paso

  1. Google Sheet — CuSu mantiene a mano el directorio (altas/bajas de contactos y una columna de estado/flag "no contactar"). Compartido como Lector con la SA del servicio (data-prod-sa@solvento-data-prod.iam.gserviceaccount.com).
  2. Cloud Scheduler (contactos-adela-sheets-etl-daily, 0 8,13,18 hora Mexico) hace POST /extract.
  3. Cloud Run contactos-adela-sheets-etl (repo) lee el Sheet vía Sheets API (pestaña por gid, detección de header por contenido), y en transform.py:
    • normaliza teléfonos a E.164 (MX +52 / US +1); lo no normalizable queda null con warning.
    • valida email y RFC; interpreta el flag de estado (CERRADA, NO CONTACTAR, … → no contactable).
    • calcula is_contactable: estado activo y hay teléfono del contacto de cobranza. Al contacto principal no se lo llama (suele ser CEO o directivo), así que no hay fallback; cuando falta el de cobranza la fila queda no contactable con el warning sin_telefono_cobranza_valido en data_warnings, para que CuSu lo cargue.
    • descarta a _rejected las filas sin RFC o con RFC duplicado.
    • emite schema_warnings si cambian las columnas del Sheet (drift).
  4. Carga a BigQuery: contactos_cobranza (truncate, foto viva) + _history (snapshot diario) + _rejected (con motivo).
  5. Consumo: la question de Metabase 6321 · Contactos Cobranza (colección CuSu, filtro opcional por customer_rfc) devuelve el directorio con is_contactable y data_warnings. El backend de Lean la consulta detrás de una capa de autenticación (patrón Consumo desde la app) e invoca al agente de ElevenLabs. ElevenLabs no pega directo a la data.

    La question no filtra is_contactable — lo tiene que respetar el backend

    Hoy 6321 devuelve todas las filas (WHERE 1=1), con el contacto principal y el de cobranza por separado. Quien decide a quién llamar es el backend, así que la marca is_contactable = false solo tiene efecto si el backend la respeta. Quitar el fallback al principal del lado del consumo es SOL-386. El archivo sql/metabase_contactos_agente.sql del repo del ETL no refleja la question desplegada.

Tablas que alimenta

Tabla Contenido Job que la carga
elevenlabs.contactos_cobranza Directorio actual, 1 fila por RFC, datos limpios Cloud Run (truncate)
elevenlabs.contactos_cobranza_history Snapshot diario (auditoría) Cloud Run (append, particionada)
elevenlabs.contactos_cobranza_rejected Filas descartadas + reject_reason Cloud Run (append, particionada)

Consumo aguas abajo

  • Agente de cobranza Adela (ElevenLabs) vía backend de Lean → question de Metabase sobre contactos_cobranza.

Si falla 🛠️

Impacto: el agente de cobranza trabaja con contactos desactualizados (última corrida válida). No bloquea otros procesos.

Síntoma Dónde mirar Acción
Tabla desactualizada Job contactos-adela-sheets-etl-daily (Cloud Scheduler) + logs del Cloud Run Ver si corrió; revisar error en el response de /extract
403 SERVICE_DISABLED al leer el Sheet Logs Cloud Run Habilitar sheets.googleapis.com en el proyecto
403 de permiso al leer el Sheet — Confirmar que el Sheet esté compartido con la SA del servicio
schema_warnings.missing no vacío Response de /extract o log SCHEMA: Alguien renombró/borró una columna del Sheet → corregir el Sheet o el mapeo en transform.py
Muchas filas en _rejected Tabla _rejected (reject_reason) Datos mal cargados en el Sheet (sin RFC / duplicados)

Escalación: proceso/código → Martin · contenido del Sheet → CuSu / María Sánchez · backend y consumo del agente → Leandro Gutiérrez.

Enlaces


Última actualización: 2026-08-25 (regla: solo se llama al contacto de cobranza) · Puente temporal hasta la migración de contactos al CRM (2026-10: HubSpot → Lightfield, destino por redefinir). Si cambia el flujo, actualizar esta ficha en el mismo MR.