""" Reconstruye los huecos en MetricasDiarias de las GACampaignMes de un mes dado. Motivo: el bug de cambio de mes en run.py (ver commit que añade merge con get_metricas_diarias_prev_month) sobrescribía el histórico del mes saliente con un dict casi vacío al redirigir la escritura del último día. Esto pasó en todos los cambios de mes hasta que se arregló, dejando huecos — a veces todo el mes salvo el último día, a veces solo el último día, a veces huecos sueltos si Airtable falló algún día concreto. Recalcula cada día que falte a partir de Google Ads (coste, conversiones) y Leads Lake (leads_lake), exactamente igual que lo habría hecho run.py cada día, y hace merge con lo que ya existe en cada registro — no toca ningún día que ya esté guardado. Uso: python backfill_metricas_mes.py # dry run python backfill_metricas_mes.py --apply # escribe en Airtable """ import calendar import json import re import sys from airtable_client import AirtableClient from google_ads_client import GoogleAdsClient def _course_num(name: str) -> str | None: m = re.search(r'fco_(?:search|pmx)_(\d+)', name, re.IGNORECASE) return m.group(1) if m else None def run(mes: int, anio: int, apply: bool): at = AirtableClient() gads = GoogleAdsClient() days_in_month = calendar.monthrange(anio, mes)[1] full_month_days = [str(d).zfill(2) for d in range(1, days_in_month + 1)] formula = f"AND({{Mes}}='{mes}',{{Año}}='{anio}')" records = at.gacampaignmes.all( formula=formula, fields=["CampaignID", "PPL", "MetricasDiarias", "Campaign Name (from CampaignID)"], ) campaigns_records = at.campaigns.all(fields=["CampaignID"]) at_id_to_gid = {r["id"]: str(r["fields"].get("CampaignID", "")).strip() for r in campaigns_records} campaigns = [] for r in records: f = r["fields"] at_cids = f.get("CampaignID", []) gid = at_id_to_gid.get(at_cids[0], "") if at_cids else "" if not gid: continue name_list = f.get("Campaign Name (from CampaignID)", []) try: md = json.loads(f.get("MetricasDiarias") or "{}") except (json.JSONDecodeError, TypeError): md = {} campaigns.append({ "airtable_id": r["id"], "google_campaign_id": gid, "curso": name_list[0] if name_list else "Sin nombre", "ppl": float(f.get("PPL") or 0), "md": md, }) print(f"→ {len(campaigns)} campañas de {mes}/{anio} en GACampaignMes") # Mapping cursoid → PMX campaign_id, igual que run.py, para atribuir leadforms cursoid_to_campaign: dict[str, str] = {} for c in campaigns: num = _course_num(c["curso"]) if num and "pmx" in c["curso"].lower() and "_leadform" not in c["curso"].lower(): cursoid_to_campaign[num] = c["google_campaign_id"] # Días que le faltan a cada campaña for c in campaigns: c["missing"] = [d for d in full_month_days if d not in c["md"]] targets = [c for c in campaigns if c["missing"]] all_missing_dates = sorted({f"{anio}-{mes:02d}-{d}" for c in targets for d in c["missing"]}) print(f"→ {len(targets)} campañas con días faltantes ({len(all_missing_dates)} fechas distintas a consultar)\n") if not targets: print("Nada que hacer.") return print(f"→ Consultando Google Ads (coste + conversiones diarias de {mes}/{anio})...") daily_metrics = gads.get_daily_metrics_for_month(anio, mes) print("→ Consultando Leads Lake por fecha...") leads_lake_by_date = {} for date_str in all_missing_dates: leads_lake_by_date[date_str] = at.get_leads_by_campaign_on_date(date_str, cursoid_to_campaign) print() metricas_updates = [] total_coste = total_ingreso = 0.0 print(f"{'Campaña':45} {'Días':>5} {'Coste rec.':>11} {'Ingreso rec.':>13} {'Margen rec.':>12}") for c in targets: cid = c["google_campaign_id"] md = dict(c["md"]) camp_coste = camp_ingreso = 0.0 for d in c["missing"]: date_str = f"{anio}-{mes:02d}-{d}" m = daily_metrics.get(date_str, {}).get(cid, {}) coste_hoy = round(m.get("cost", 0), 2) conv_hoy = m.get("conversions", 0) ingreso_hoy = round(conv_hoy * c["ppl"], 2) margen_hoy = round(ingreso_hoy - coste_hoy, 2) leads_lake_hoy = leads_lake_by_date.get(date_str, {}).get(cid, 0) md[d] = { "coste": coste_hoy, "ingreso": ingreso_hoy, "margen": margen_hoy, "leads": int(conv_hoy), "leads_lake": leads_lake_hoy, } camp_coste += coste_hoy camp_ingreso += ingreso_hoy total_coste += camp_coste total_ingreso += camp_ingreso print(f"{c['curso'][:45]:45} {len(c['missing']):>5} {camp_coste:>10.2f}€ " f"{camp_ingreso:>12.2f}€ {camp_ingreso - camp_coste:>11.2f}€") metricas_updates.append({ "airtable_id": c["airtable_id"], "metricas_json": json.dumps(md, ensure_ascii=False, sort_keys=True), }) print(f"\n{'TOTAL recuperado':45} {'':>5} {total_coste:>10.2f}€ {total_ingreso:>12.2f}€ " f"{total_ingreso - total_coste:>11.2f}€") print() if apply: at.batch_update_metricas_diarias(metricas_updates) print(f"✅ {len(metricas_updates)} registros actualizados en Airtable.") else: print(f"DRY RUN — {len(metricas_updates)} registros se actualizarían. " f"Ejecuta con --apply para escribir en Airtable.") if __name__ == "__main__": args = [a for a in sys.argv[1:] if not a.startswith("--")] if len(args) != 2: print("Uso: python backfill_metricas_mes.py [--apply]") sys.exit(1) run(mes=int(args[0]), anio=int(args[1]), apply="--apply" in sys.argv)