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
monthlyByInstitutiondel 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)
- Resuelve qué entidades procesar. En modo incremental (diario
15 5 * * *, jobsyntage-moratory-interest-daily— corre 15 min después delsyntage-entities-sync-dailyde las0 5, respetando la dependencia del catálogo) toma solo el delta: entidades con facturas nuevas en los últimosINCREMENTAL_LOOKBACK_DAYS, vía el JOINinvoices ↔ entities. El backfill (carga inicial) recorre todas las entidades concredential_status = 'valid'y es resumible por cursor (PROCESS_BUDGET_S, ~50 min por invocación; respondehas_morehasta terminar). - Consulta la API de Syntage
GET /entities/{entityId}/insights/moratory-interestcon 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 ante429. - Escribe en BigQuery el bloque
monthlyByInstitutionensyntage.moratory_interest(append) y una fila por corrida ensyntage.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_RANKporentity_idordenandoextracted_at DESC, snapshot_id DESC), redondeatotal_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
- 📦 Ingesta:
syntage-moratory-interest· dependenciasyntage-entities-sync - 🧮 Lógica:
dbt-etls—models/advanced_analytics/moratory_interest.sql - 📊 Estado en vivo: dashboard "Data Processes" (Cloud Monitoring)
Última actualización: 2026-06-21 · Si cambia el flujo o el modelo dbt, actualizar esta ficha en el mismo MR.