""" Calcula métricas derivadas y urgencia para una campaña de Meta Ads ligada a un curso. Puerto directo de leads-optimizer/analyzer.py: las fórmulas de PPL, capping y ritmo son agnósticas de canal (Google/Meta), solo cambian los nombres de los campos de origen de las métricas de gasto/conversiones. """ from datetime import datetime import calendar def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) -> dict: now = datetime.now() dias_mes = calendar.monthrange(now.year, now.month)[1] dia_actual = now.day ratio_mes = dia_actual / dias_mes capping = campaign_config["capping_mensual"] ppl = campaign_config["ppl"] cpa_max = campaign_config["cpa_maximo"] gasto = ads_metrics.get("spend", 0) conversiones_meta = ads_metrics.get("leads", 0) ratio_leads = leads_entregados / capping if capping > 0 else 0 cpa_actual = gasto / leads_entregados if leads_entregados > 0 else 0 revenue = leads_entregados * ppl margen = (revenue - gasto) / revenue if revenue > 0 else 0 leads_restantes = capping - leads_entregados dias_restantes = dias_mes - dia_actual ritmo = ratio_leads - ratio_mes # positivo = adelantado, negativo = atrasado if ratio_leads >= 1.0: urgencia = "PAUSAR" elif capping > 0 and ratio_leads < ratio_mes - 0.15 and dias_restantes <= 5: urgencia = "SPRINT" elif ritmo < -0.15: urgencia = "ACELERAR" elif ritmo > 0.15: urgencia = "FRENAR" else: urgencia = "EN_RITMO" # Discrepancia en ambas direcciones entre el conteo propio de Meta y el de # Leads Lake (Airtable) — puede indicar leads fantasma o tracking roto. conv_leads_lake_mes = campaign_config.get("conv_leads_lake_mes", leads_entregados) discrepancia = abs(conversiones_meta - conv_leads_lake_mes) return { "curso": campaign_config["curso"], "campaign_id": campaign_config["meta_campaign_id"], "ppl": ppl, "cpa_maximo": cpa_max, "capping": capping, "leads_entregados": leads_entregados, "leads_restantes": leads_restantes, "dias_restantes": dias_restantes, "ratio_leads": round(ratio_leads, 3), "ratio_mes": round(ratio_mes, 3), "ritmo": round(ritmo, 3), "urgencia": urgencia, "cpa_actual": round(cpa_actual, 2), "rentable": cpa_actual <= cpa_max if cpa_actual > 0 else True, "margen": round(margen, 3), "revenue_estimado": round(revenue, 2), "gasto_acumulado": round(gasto, 2), "budget_diario_actual": ads_metrics.get("budget_daily", 0), "ctr": ads_metrics.get("ctr", 0), "clicks": ads_metrics.get("clicks", 0), "conversiones_meta": conversiones_meta, "discrepancia_tracking": discrepancia, "alerta_tracking": discrepancia > 10, "status_meta": ads_metrics.get("status", "UNKNOWN"), }