mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 18:48:47 +02:00
The n8n flows fail invisibly from the app's perspective (review UX-17, the top trust gap). /api/v1/sync-status derives last-received + cadence thresholds for BAC, salary, municipal, pensions and exchange rate from existing data — no n8n integration needed. New Sincronización page shows per-source freshness and a hint to check n8n; the sidebar item gets an amber dot when any source is stale. Verified live against the dev DB (correctly flags its stale BAC data at 43 days). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
"""Per-source ingestion health, derived from existing data (review UX-17).
|
|
|
|
The n8n flows fail silently from the app's perspective — a parser break just
|
|
looks like missing data. This derives "when did we last receive something from
|
|
each source" plus a cadence threshold, so a stalled flow becomes visible
|
|
in-app without any n8n integration.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from sqlmodel import Session, func, select
|
|
|
|
from app.models.models import (
|
|
Bank,
|
|
ExchangeRate,
|
|
MunicipalReceipt,
|
|
PensionSnapshot,
|
|
Transaction,
|
|
TransactionSource,
|
|
TransactionType,
|
|
)
|
|
from app.timeutil import utcnow
|
|
|
|
|
|
@dataclass
|
|
class SourceSpec:
|
|
key: str
|
|
label: str
|
|
warn_after_days: float
|
|
description: str
|
|
|
|
|
|
# Cadences: BAC emails arrive near-daily with normal card use; salary is
|
|
# biweekly/monthly; municipal and pension are monthly; rates refresh every 6h.
|
|
SOURCES: list[SourceSpec] = [
|
|
SourceSpec("bac_credit_card", "BAC Tarjeta de Crédito", 7,
|
|
"Correos de compra parseados por n8n"),
|
|
SourceSpec("salary", "Depósitos de Salario", 35,
|
|
"Correos de depósito parseados por n8n"),
|
|
SourceSpec("municipal", "Recibos Municipales", 40,
|
|
"PDF mensual vía n8n (cron)"),
|
|
SourceSpec("pensions", "Estados de Pensión", 40,
|
|
"PDFs diarios/mensuales vía n8n"),
|
|
SourceSpec("exchange_rate", "Tipo de Cambio", 1,
|
|
"Actualización automática cada 6 horas"),
|
|
]
|
|
|
|
|
|
def _last_received(session: Session, key: str):
|
|
if key == "bac_credit_card":
|
|
return session.exec(
|
|
select(func.max(Transaction.created_at)).where(
|
|
Transaction.source == TransactionSource.CREDIT_CARD,
|
|
Transaction.bank == Bank.BAC,
|
|
)
|
|
).one()
|
|
if key == "salary":
|
|
return session.exec(
|
|
select(func.max(Transaction.created_at)).where(
|
|
Transaction.transaction_type == TransactionType.SALARY
|
|
)
|
|
).one()
|
|
if key == "municipal":
|
|
return session.exec(select(func.max(MunicipalReceipt.created_at))).one()
|
|
if key == "pensions":
|
|
return session.exec(select(func.max(PensionSnapshot.created_at))).one()
|
|
if key == "exchange_rate":
|
|
return session.exec(select(func.max(ExchangeRate.fetched_at))).one()
|
|
raise ValueError(f"unknown sync source: {key}")
|
|
|
|
|
|
def compute_sync_status(session: Session) -> list[dict]:
|
|
now = utcnow()
|
|
out: list[dict] = []
|
|
for spec in SOURCES:
|
|
last = _last_received(session, spec.key)
|
|
if last is None:
|
|
status = "never"
|
|
age_days = None
|
|
else:
|
|
age_days = (now - last).total_seconds() / 86400
|
|
status = "ok" if age_days <= spec.warn_after_days else "warning"
|
|
out.append(
|
|
{
|
|
"key": spec.key,
|
|
"label": spec.label,
|
|
"description": spec.description,
|
|
"last_received": last.isoformat() if last else None,
|
|
"age_days": round(age_days, 1) if age_days is not None else None,
|
|
"warn_after_days": spec.warn_after_days,
|
|
"status": status,
|
|
}
|
|
)
|
|
return out
|