diff --git a/airtable_client.py b/airtable_client.py index 99efe21..e0d4593 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -339,11 +339,12 @@ class AirtableClient: def get_leads_this_month_gads(self, campaign_id: str, campaign_name: str = "") -> tuple[int, list[str]]: """ Leads del mes actual para una campaña de Google Ads. - Cubre cuatro vías de atribución: + Cubre cinco vías de atribución: 1. GACampaignID / GoogleCampaignID (leads web normales) 2. gad_campaignid en attr_referer (UTM web) - 3. attr_referer = campaign_name con UserAgent Google-Ads-Notifications (Lead Form) - 4. attr_cursoid = course_num AND attr_utm_source = 'pmx'|'google' (atribución por curso) + 3. attr_referer = campaign_name con UserAgent Google-Ads-Notifications (Lead Form por nombre) + 4. attr_cursoid = course_num AND attr_utm_source = 'pmx'|'google' (atribución web por curso) + 5. attr_cursoid = course_num AND UserAgent = 'Google-Ads-Notifications' (Lead Form por cursoid) """ now = datetime.now() mes_inicio = f"{now.year}-{now.month:02d}-01" @@ -353,8 +354,8 @@ class AirtableClient: if campaign_name else "FALSE()" ) - # Extraer número de curso y tipo de fuente del nombre de campaña curso_clause = "FALSE()" + leadform_cursoid_clause = "FALSE()" m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE) if m: course_num = m.group(1) @@ -369,6 +370,10 @@ class AirtableClient: f"AND({{attr_cursoid}}='{course_num}'," f"{{attr_utm_source}}='{utm_source}')" ) + leadform_cursoid_clause = ( + f"AND({{attr_cursoid}}='{course_num}'," + f"{{UserAgent del visitante}}='Google-Ads-Notifications')" + ) formula = ( f"AND(" @@ -377,7 +382,8 @@ class AirtableClient: f"FIND(',{campaign_id},',',' & {{GoogleCampaignID}} & ',')," f"FIND('gad_campaignid={campaign_id}',{{attr_referer}})," f"{leadform_clause}," - f"{curso_clause}" + f"{curso_clause}," + f"{leadform_cursoid_clause}" f")," f"{{creado}}>='{mes_inicio}'" f")" @@ -386,18 +392,19 @@ 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: + def get_leads_by_campaign_on_date(self, date_str: str, cursoid_to_campaign: dict = None) -> 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. + Una sola llamada bulk para todas las campañas. + cursoid_to_campaign: {course_num: google_campaign_id} para atribuir leadforms por cursoid. 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"], - ) + fields = ["GACampaignID", "GoogleCampaignID", "attr_referer"] + if cursoid_to_campaign: + fields += ["attr_cursoid", "UserAgent del visitante"] + records = self.leads.all(formula=formula, fields=fields) counts: dict[str, int] = {} for r in records: f = r["fields"] @@ -408,6 +415,10 @@ class AirtableClient: m = re.search(r"gad_campaignid=(\d+)", f.get("attr_referer", "")) if m: cid = m.group(1) + if not cid and cursoid_to_campaign: + if f.get("UserAgent del visitante") == "Google-Ads-Notifications": + cursoid = str(f.get("attr_cursoid") or "").strip() + cid = cursoid_to_campaign.get(cursoid, "") if cid: counts[cid] = counts.get(cid, 0) + 1 return counts diff --git a/backfill_leadform_jun8_10.py b/backfill_leadform_jun8_10.py new file mode 100644 index 0000000..a31d7bd --- /dev/null +++ b/backfill_leadform_jun8_10.py @@ -0,0 +1,97 @@ +""" +Recalcula leads_lake en MetricasDiarias para días 8-10 de junio 2026 +añadiendo la atribución de leadforms via cursoid + UserAgent. +También actualiza ConvLeadsLakeMes, Leads Lake y ConvLeadsLakeMesFinal. +""" +import json +import re +from datetime import datetime +from airtable_client import AirtableClient + +DAYS = ["2026-06-08", "2026-06-09", "2026-06-10"] + + +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(): + at = AirtableClient() + + campaigns = at.get_active_gacampaignmes() + print(f"→ {len(campaigns)} campañas activas este mes\n") + + # Mapping cursoid → PMX campaign_id para leadforms diarios + 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"] + print(f"→ Mapping cursoid→campaign: {len(cursoid_to_campaign)} entradas") + + # Obtener leads por campaña para cada día (una llamada bulk por día) + daily_counts: dict[str, dict[str, int]] = {} + for date_str in DAYS: + daily_counts[date_str] = at.get_leads_by_campaign_on_date(date_str, cursoid_to_campaign) + total = sum(daily_counts[date_str].values()) + print(f" {date_str}: {total} leads totales encontrados") + print() + + # Actualizar MetricasDiarias para cada campaña + metricas_updates = [] + for campaign in campaigns: + cid = campaign["google_campaign_id"] + try: + md = json.loads(campaign["metricas_diarias"]) + except (json.JSONDecodeError, TypeError): + md = {} + + changed = False + for date_str in DAYS: + day_key = date_str[8:10] + if day_key not in md: + continue + new_count = daily_counts[date_str].get(cid, 0) + old_count = md[day_key].get("leads_lake", 0) + if old_count != new_count: + print(f" {campaign['curso'][:45]} día {day_key}: leads_lake {old_count} → {new_count}") + md[day_key]["leads_lake"] = new_count + # Recalcular ingreso_lxp para el día (leads_lake × PPL) + md[day_key]["ingreso"] = round(new_count * campaign["ppl"], 2) + changed = True + + if changed: + metricas_updates.append({ + "airtable_id": campaign["airtable_id"], + "metricas_json": json.dumps(md, ensure_ascii=False), + }) + + if metricas_updates: + print(f"\n→ Actualizando MetricasDiarias ({len(metricas_updates)} registros)...") + at.batch_update_metricas_diarias(metricas_updates) + print(" ✓ MetricasDiarias actualizado.") + else: + print(" ✓ MetricasDiarias sin cambios.") + + # Recalcular totales mensuales con la nueva atribución + print("\n→ Recalculando totales mensuales (leads_lake acumulado del mes)...") + final_leads_data = [] + for campaign in campaigns: + cid = campaign["google_campaign_id"] + leads, lead_ids = at.get_leads_this_month_gads(cid, campaign["curso"]) + at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids) + final_leads_data.append({ + "airtable_id": campaign["airtable_id"], + "conv_leads_lake_mes": leads, + "conv_leads_lake_mes_grupo": leads, + }) + if leads > 0: + print(f" {campaign['curso'][:45]}: {leads} leads") + + at.batch_update_gacampaignmes_final_leads(final_leads_data) + print(" ✓ ConvLeadsLakeMesFinal actualizado.") + + +if __name__ == "__main__": + run() diff --git a/run.py b/run.py index 83b9458..2881c4f 100644 --- a/run.py +++ b/run.py @@ -101,7 +101,6 @@ def run(): monthly_metrics = gads.get_monthly_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) @@ -165,6 +164,17 @@ def run(): lf_conv = int(monthly_metrics.get(c["google_campaign_id"], {}).get("conversions", 0)) leadform_conv_by_course[num] = leadform_conv_by_course.get(num, 0) + lf_conv + # Mapping cursoid → PMX campaign_id para atribuir leadforms diarios por cursoid + 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"] + + leads_yesterday = at.get_leads_by_campaign_on_date( + _ayer_date.strftime("%Y-%m-%d"), cursoid_to_campaign + ) + # === PRIMERA PASADA: recopilar datos de todas las campañas === collected = [] skipped = []