Skip to content

Syntage · Insights — Intereses moratorios

Extrae diariamente de la API de Syntage el insight de intereses moratorios por institución de cada entidad, lo guarda como snapshots históricos en BigQuery, y dbt lo reduce al estado vigente.

¿Llegaste hasta acá desde…?

Si ves… Eso sale de… La lógica está en…
Intereses moratorios por entidad/mes/institución en un dashboard solvento-adv-analytics-prod.consistency_jobs.moratory_interest dbt-etls → models/advanced_analytics/moratory_interest.sql
El histórico de cómo cambió un moratorio entre extracciones solvento-data-prod.syntage.moratory_interest (RAW, append) Ingesta — Cloud Run syntage-moratory-interest
"¿Esta entidad fue consultada o no tenía moratorios?" solvento-data-prod.syntage.moratory_interest_run_control Tabla de control (no el fact)

El concepto (por qué está diseñado así)

  • Grano atómico. Se guarda solo el bloque monthlyByInstitution del insight, una fila por (snapshot, entidad, mes, institución). Los bloques agregados del insight (monthly, institutions, summary) no se guardan: se derivan downstream en dbt. Así la métrica oficial se define una sola vez en la capa de métricas, sin KPIs como caja negra.
  • Snapshots históricos. El insight devuelve una ventana móvil de ~12 meses que cambia en cada llamada. Por eso la RAW es append: cada corrida agrega un snapshot etiquetado con snapshot_id + extracted_at. Permite ver cómo evoluciona el dato de una entidad entre extracciones.
  • "Último snapshot gana" (en dbt). Para el estado vigente se toma el snapshot más reciente completo de cada entidad, no un dedup por celda. Si un (mes, institución) desaparece del último snapshot, debe desaparecer del resultado — un merge por clave dejaría "celdas fantasma".
  • Consultada-y-vacía ≠ no-consultada. Una entidad sin moratorios no escribe filas en el fact; que fue consultada queda en la tabla de control.

Diagrama de linaje

graph LR
    subgraph dep["Dependencias (deben estar frescas)"]
        ENT[("syntage.entities<br/><i>rfc → entity_id</i>")]
        INV[("syntage.invoices<br/><i>delta diario</i>")]
    end

    API[["🌐 API Syntage<br/>/insights/moratory-interest"]]

    subgraph ing["1 · Ingesta · Cloud Run programado"]
        CR["syntage-moratory-interest<br/><i>incremental diario · backfill resumible</i>"]
        RAW[("solvento-data-prod<br/>syntage.moratory_interest<br/>append · snapshots")]
        CTRL[("…moratory_interest_<br/>run_control")]
    end

    subgraph logica["2 · Lógica · dbt-etls"]
        M["moratory_interest.sql<br/><i>último snapshot por entidad</i>"]
        OUT[("solvento-adv-analytics-prod<br/>consistency_jobs.moratory_interest")]
    end

    CONS["📊 Métricas / Metabase"]

    ENT --> CR
    INV --> CR
    API --> CR
    CR --> RAW
    CR --> CTRL
    RAW --> M --> OUT --> CONS

    classDef src fill:#fde68a,stroke:#b45309,color:#000;
    classDef store fill:#bfdbfe,stroke:#1e40af,color:#000;
    classDef logic fill:#dcfce7,stroke:#15803d,color:#000;
    class API src;
    class RAW,CTRL,OUT,ENT,INV store;
    class M logic;

Paso a paso

1 · Ingesta — Cloud Run syntage-moratory-interest (repo)

  1. Resuelve qué entidades procesar. En modo incremental (diario 15 5 * * *, job syntage-moratory-interest-daily — corre 15 min después del syntage-entities-sync-daily de las 0 5, respetando la dependencia del catálogo) toma solo el delta: entidades con facturas nuevas en los últimos INCREMENTAL_LOOKBACK_DAYS, vía el JOIN invoices ↔ entities. El backfill (carga inicial) recorre todas las entidades con credential_status = 'valid' y es resumible por cursor (PROCESS_BUDGET_S, ~50 min por invocación; responde has_more hasta terminar).
  2. Consulta la API de Syntage GET /entities/{entityId}/insights/moratory-interest con pacing (REQS_PER_MINUTE, default 30 de los 60/min permitidos, para no ahogar a los procesos críticos que comparten la API) y backoff ante 429.
  3. Escribe en BigQuery el bloque monthlyByInstitution en syntage.moratory_interest (append) y una fila por corrida en syntage.moratory_interest_run_control.

Dependencia de orden: debe correr después del syntage-entities-sync diario, para que una entidad que facturó hoy ya tenga su entity_id en el catálogo syntage.entities.

2 · Lógica — dbt moratory_interest.sql (repo dbt-etls)

  • Reduce la RAW al último snapshot de cada entidad (DENSE_RANK por entity_id ordenando extracted_at DESC, snapshot_id DESC), redondea total_amount, y materializa como table → solvento-adv-analytics-prod.consistency_jobs.moratory_interest. Debe correr después de la corrida diaria del servicio.

3 · Consumo

  • Métricas / Metabase leen de consistency_jobs.moratory_interest. Los agregados (monthly, summary) se calculan en la capa de métricas, no se persisten desde el origen.

Si falla 🛠️

Impacto: los moratorios quedan con el último snapshot bueno; criticidad media (insight analítico, no bloquea producto).

Síntoma Dónde mirar Acción
No se actualizan moratorios Logs Cloud Run syntage-moratory-interest; tabla …run_control (status, has_more) Re-invocar POST /process. Si el backfill quedó a medias, reanuda solo por cursor
Faltan entidades nuevas ¿Corrió syntage-entities-sync antes? syntage.entities desactualizado Correr primero entities-sync; el insight requiere entity_id
Errores 429 / rate limit X-RateLimit-* en logs El servicio ya hace backoff; si persiste, bajar REQS_PER_MINUTE
Dato vigente "raro" / celdas viejas Lógica dbt moratory_interest.sql Verificar que corrió después del servicio y que el modelo es table (no incremental)

Escalación: lógica SQL → Diego · resto → Martin.

Enlaces


Última actualización: 2026-06-21 · Si cambia el flujo o el modelo dbt, actualizar esta ficha en el mismo MR.