diff --git a/google_ads_client.py b/google_ads_client.py index 49c86de..df0b82c 100644 --- a/google_ads_client.py +++ b/google_ads_client.py @@ -133,6 +133,66 @@ class GoogleAdsClient: print(f" ❌ Error obteniendo métricas mensuales: {e}") return result + def get_monthly_metrics_all(self) -> dict: + """ + Métricas del mes en curso para TODAS las campañas en una sola query. + Retorna dict {campaign_id: {cost, conversions, clicks, impressions, ctr, + status, budget_daily, budget_resource_name, name}}. + """ + ga_service = self.client.get_service("GoogleAdsService") + query = """ + SELECT + campaign.id, + campaign.name, + campaign.status, + campaign_budget.amount_micros, + campaign_budget.resource_name, + metrics.cost_micros, + metrics.conversions, + metrics.clicks, + metrics.impressions + FROM campaign + WHERE campaign.status != 'REMOVED' + AND segments.date DURING THIS_MONTH + """ + raw: dict = {} + try: + response = ga_service.search(customer_id=self.customer_id, query=query) + for row in response: + cid = str(row.campaign.id) + if cid not in raw: + raw[cid] = { + "name": row.campaign.name, + "status": row.campaign.status.name, + "budget_daily": row.campaign_budget.amount_micros / 1_000_000, + "budget_resource_name": row.campaign_budget.resource_name, + "cost": 0.0, "conversions": 0.0, "clicks": 0, "impressions": 0, + } + m = row.metrics + raw[cid]["cost"] += m.cost_micros / 1_000_000 + raw[cid]["conversions"] += m.conversions + raw[cid]["clicks"] += m.clicks + raw[cid]["impressions"] += m.impressions + except GoogleAdsException as e: + print(f" ❌ Error obteniendo métricas mensuales bulk: {e}") + + result = {} + for cid, d in raw.items(): + imp = d["impressions"] + result[cid] = { + "campaign_id": cid, + "name": d["name"], + "status": d["status"], + "budget_daily": round(d["budget_daily"], 2), + "budget_resource_name": d["budget_resource_name"], + "cost": round(d["cost"], 2), + "conversions": d["conversions"], + "clicks": d["clicks"], + "impressions": imp, + "ctr": round(d["clicks"] / imp * 100, 2) if imp > 0 else 0.0, + } + return result + def get_campaign_metrics(self, campaign_id: str) -> dict: """Métricas del mes en curso para una campaña concreta (acumulado mensual).""" ga_service = self.client.get_service("GoogleAdsService") diff --git a/run.py b/run.py index 2f5ddc1..83b9458 100644 --- a/run.py +++ b/run.py @@ -156,6 +156,15 @@ def run(): if "_leadform" in c["curso"].lower() and _course_num(c["curso"]) } + # Precomputar conversiones Google de cada campaña leadform por número de curso + leadform_conv_by_course: dict[str, int] = {} + for c in campaigns: + if "_leadform" in c["curso"].lower(): + num = _course_num(c["curso"]) + if num: + 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 + # === PRIMERA PASADA: recopilar datos de todas las campañas === collected = [] skipped = [] @@ -167,7 +176,7 @@ def run(): leads, lead_ids = at.get_leads_this_month_gads(cid, campaign["curso"]) at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids) - metrics = gads.get_campaign_metrics(cid) + metrics = monthly_metrics.get(cid) if not metrics: skipped.append(f"[{cid}] {campaign['curso']}") continue @@ -181,13 +190,20 @@ def run(): google_status, )) - # Para PMX con companion Search: usar conversiones Google como leads de análisis course_num = _course_num(campaign["curso"]) - is_pmx_with_companion = ( - "pmx" in campaign["curso"].lower() - and course_num in courses_with_both - ) - leads_grupo = int(metrics.get("conversions", 0)) if is_pmx_with_companion else leads + is_pmx = "pmx" in campaign["curso"].lower() and "_leadform" not in campaign["curso"].lower() + + # PMX con companion Search: conversiones Google del PMX (search fluye hacia PMX) + is_pmx_with_companion = is_pmx and course_num in courses_with_both + # PMX con companion Leadform: conv PMX + conv leadform (leads no llegan a Airtable) + is_pmx_with_leadform = is_pmx and course_num in courses_with_leadform + + if is_pmx_with_leadform: + leads_grupo = int(metrics.get("conversions", 0)) + leadform_conv_by_course.get(course_num, 0) + elif is_pmx_with_companion: + leads_grupo = int(metrics.get("conversions", 0)) + else: + leads_grupo = leads analysis = analyze(campaign, leads_grupo, metrics) decision = decide(analysis) @@ -200,6 +216,7 @@ def run(): "analysis": analysis, "decision": decision, "today_metrics": today_metrics.get(cid, {}), + "is_pmx_with_leadform": is_pmx_with_leadform, }) # Actualizar status en GACampaignMes y Google Ads Campaigns @@ -293,8 +310,9 @@ def run(): is_leadform = "_leadform" in campaign["curso"].lower() if is_leadform: log_text += " | ℹ️ LEADFORM: leads capturados directamente en Google (sin visitar la web) — no llegan a Airtable" - elif course_num in courses_with_leadform: - log_text += " | ⚠️ LEADFORM COMPANION: existe una campaña _leadform activa para este curso — parte de las conversiones de Google pueden provenir de leads capturados directamente en Google" + elif item.get("is_pmx_with_leadform"): + lf_conv = leadform_conv_by_course.get(course_num, 0) + log_text += f" | ℹ️ LEADFORM COMPANION: {lf_conv} conv leadform sumadas a leads_grupo para este curso" # Métricas diarias: coste hoy, ingreso (conversiones × PPL) y margen coste_hoy = round(today_m.get("cost", 0), 2)