diff --git a/airtable_client.py b/airtable_client.py index 224eefa..783fade 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -1,6 +1,7 @@ from pyairtable import Api -from datetime import datetime +from datetime import datetime, timedelta import json +import re import config @@ -363,6 +364,32 @@ class AirtableClient: ids = [r["id"] for r in records] return len(ids), ids + def get_leads_by_campaign_on_date(self, date_str: str) -> dict: + """ + Devuelve {google_campaign_id: count} para todos los leads de un día concreto. + Una sola llamada bulk para todos los campañas — más eficiente que una por campaña. + date_str: 'YYYY-MM-DD' + """ + next_day = (datetime.strptime(date_str, "%Y-%m-%d") + timedelta(days=1)).strftime("%Y-%m-%d") + formula = f"AND({{creado}}>='{date_str}',{{creado}}<'{next_day}')" + records = self.leads.all( + formula=formula, + fields=["GACampaignID", "GoogleCampaignID", "attr_referer"], + ) + counts: dict[str, int] = {} + for r in records: + f = r["fields"] + cid = str(f.get("GACampaignID") or "").strip() + if not cid: + cid = str(f.get("GoogleCampaignID") or "").strip() + if not cid: + m = re.search(r"gad_campaignid=(\d+)", f.get("attr_referer", "")) + if m: + cid = m.group(1) + if cid: + counts[cid] = counts.get(cid, 0) + 1 + return counts + def update_gacampaignmes_leads_lake(self, gcm_record_id: str, lead_ids: list[str]) -> None: """Actualiza el campo 'Leads Lake' de un registro GACampaignMes.""" self.gacampaignmes.update(gcm_record_id, {"Leads Lake": lead_ids}) diff --git a/backfill_leads_mayo.py b/backfill_leads_mayo.py new file mode 100644 index 0000000..b9a2e08 --- /dev/null +++ b/backfill_leads_mayo.py @@ -0,0 +1,79 @@ +""" +Script one-off: rellena el campo 'leads' en MetricasDiarias para cada día +de mayo ya registrado en GACampaignMes. + +Ejecutar una sola vez: + python backfill_leads_mayo.py +""" +import json +from datetime import datetime, timedelta +from airtable_client import AirtableClient + +at = AirtableClient() +now = datetime.now() + +print("Cargando registros GACampaignMes de mayo...") +records = at.gacampaignmes.all( + formula="AND({Mes}='5',{Año}='2026')", + fields=["CampaignID", "MetricasDiarias", "Campaign Name (from CampaignID)"], +) + +# Mapping google_campaign_id → (gcm_record_id, metricas_dict) +gid_to_gcm: dict[str, tuple[str, dict]] = {} +campaigns_records = at.campaigns.all(fields=["CampaignID"]) +at_id_to_gid = {r["id"]: str(r["fields"].get("CampaignID", "")).strip() for r in campaigns_records} + +for r in records: + at_cids = r["fields"].get("CampaignID", []) + if not at_cids: + continue + gid = at_id_to_gid.get(at_cids[0], "") + if not gid: + continue + try: + md = json.loads(r["fields"].get("MetricasDiarias") or "{}") + except (json.JSONDecodeError, TypeError): + md = {} + gid_to_gcm[gid] = (r["id"], md) + +# Días de mayo que ya tienen métricas (sin 'leads') +days_needed: set[str] = set() +for _, (_, md) in gid_to_gcm.items(): + for day_str, vals in md.items(): + if "leads" not in vals: + days_needed.add(day_str) + +if not days_needed: + print("Todos los días ya tienen 'leads'. Nada que hacer.") + exit(0) + +print(f"Días a rellenar: {sorted(days_needed)}") + +# Para cada día, obtener leads bulk +for day_str in sorted(days_needed): + try: + day_int = int(day_str) + date = datetime(now.year, 5, day_int) + except ValueError: + continue + date_iso = date.strftime("%Y-%m-%d") + print(f" > {date_iso} ...", end=" ", flush=True) + leads_by_cid = at.get_leads_by_campaign_on_date(date_iso) + updated = 0 + for gid, (gcm_id, md) in gid_to_gcm.items(): + if day_str in md and "leads" not in md[day_str]: + md[day_str]["leads"] = leads_by_cid.get(gid, 0) + updated += 1 + print(f"{updated} campañas actualizadas") + +# Guardar en Airtable en lotes +print("\nGuardando en Airtable...") +batch = [ + {"id": gcm_id, "fields": {"MetricasDiarias": json.dumps(md, ensure_ascii=False)}} + for _, (gcm_id, md) in gid_to_gcm.items() + if any("leads" in v for v in md.values()) +] +for i in range(0, len(batch), 10): + at.gacampaignmes.batch_update(batch[i:i+10]) + +print(f"✓ Backfill completado: {len(batch)} registros actualizados.") diff --git a/run.py b/run.py index 17e975b..43f408e 100644 --- a/run.py +++ b/run.py @@ -12,7 +12,7 @@ from agent import decide from optimizer import apply_decision from slack_reporter import build_and_send import config -from datetime import datetime +from datetime import datetime, timedelta class Tee: @@ -99,7 +99,9 @@ def run(): print("→ Sincronizando campañas desde Google Ads...") google_campaigns = gads.get_all_campaigns() monthly_metrics = gads.get_monthly_metrics_all() - today_metrics = gads.get_yesterday_metrics_all() + today_metrics = gads.get_yesterday_metrics_all() + _ayer_date = (datetime.now() - timedelta(days=1)) + leads_yesterday = at.get_leads_by_campaign_on_date(_ayer_date.strftime("%Y-%m-%d")) print(" Calculando PPL y CapTotalMes...") ppl_lookup, cap_lookup = at.build_campaign_lookups() sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup, cap_lookup) @@ -217,7 +219,6 @@ def run(): resumen = [] advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final metricas_updates = [] # {airtable_id, metricas_json} para MetricasDiarias - from datetime import timedelta ayer = datetime.now() - timedelta(days=1) dia_hoy = ayer.strftime("%d") cambio_mes = ayer.month != datetime.now().month @@ -302,7 +303,8 @@ def run(): md = json.loads(campaign["metricas_diarias"]) except (json.JSONDecodeError, TypeError): md = {} - md[dia_hoy] = {"coste": coste_hoy, "ingreso": ingreso_hoy, "margen": margen_hoy} + leads_hoy = leads_yesterday.get(cid, 0) + md[dia_hoy] = {"coste": coste_hoy, "ingreso": ingreso_hoy, "margen": margen_hoy, "leads": leads_hoy} campaign["metricas_diarias"] = json.dumps(md, ensure_ascii=False) metricas_updates.append({ "airtable_id": campaign["airtable_id"],