""" Reconstruye los huecos en MetricasDiarias de las GACampaignMes de junio 2026: - 35 campañas perdieron los días 01-29 (el bug de cambio de mes sobrescribió su histórico completo dejando solo el día 30). - 32 campañas no tienen el día 30 (nunca se llegó a escribir — coste y conversiones fueron 0 ese día, confirmado contra Google Ads). 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 los días que ya están bien. Uso: python backfill_metricas_junio_completo.py # dry run python backfill_metricas_junio_completo.py --apply # escribe en Airtable """ import json import re import sys from airtable_client import AirtableClient from google_ads_client import GoogleAdsClient MES, ANIO = 6, 2026 FULL_MONTH_DAYS = [str(d).zfill(2) for d in range(1, 31)] 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(apply: bool): at = AirtableClient() gads = GoogleAdsClient() 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("→ Consultando Google Ads (coste + conversiones diarias de junio)...") 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__": run(apply="--apply" in sys.argv)