"""Re-send yesterday's Slack report from Baserow snapshots.""" import json from datetime import datetime, timedelta import config from meta_ads_client import MetaAdsClient from baserow_client import BaserowClient import slack_notifier def _extract_vertical(name: str) -> str: prefix = config.META_CAMPAIGN_PREFIX rest = name[len(prefix):].lstrip("_") parts = rest.split("_") start = 1 if parts and parts[0].isdigit() else 0 return parts[start].lower() if start < len(parts) else "otros" def main(): import sys as _sys run_date = _sys.argv[1] if len(_sys.argv) > 1 else (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") print(f"Reenviando informe para {run_date}...") meta = MetaAdsClient() baserow = BaserowClient() # ── Vertical CPL targets ────────────────────────────────────────────────── vertical_cpls: dict = {} try: for v in baserow.get_all_verticals(): name = (v.get("Nombre") or "").strip().lower() cpl = float(v.get("target_cpl") or 0) if name and cpl: vertical_cpls[name] = cpl print(f" ✓ Verticales: {vertical_cpls}") except Exception as e: print(f" ⚠ No se pudieron cargar verticales: {e}") # ── Monthly daily totals ────────────────────────────────────────────────── print("Obteniendo datos mensuales de Meta...") daily_rows = meta.get_monthly_daily_totals() _daily: dict = {} monthly_verticals: dict = {} for row in daily_rows: v = _extract_vertical(row["campaign_name"]) target = vertical_cpls.get(v, config.META_TARGET_CPL) margin = ( round((target - row["spend"] / row["leads"]) * row["leads"], 2) if row["leads"] > 0 else round(-row["spend"], 2) ) d = _daily.setdefault(row["date"], {"spend": 0.0, "leads": 0, "margin": 0.0}) d["spend"] += row["spend"] d["leads"] += row["leads"] d["margin"] += margin mv = monthly_verticals.setdefault(v, {"spend": 0.0, "leads": 0, "margin": 0.0, "target_cpl": target}) mv["spend"] += row["spend"] mv["leads"] += row["leads"] mv["margin"] += margin daily_totals = [ { "date": date, "spend": round(d["spend"], 2), "leads": int(d["leads"]), "cpl": round(d["spend"] / d["leads"], 2) if d["leads"] > 0 else 0.0, "margin": round(d["margin"], 2), } for date, d in sorted(_daily.items()) ] print(f" ✓ {len(daily_totals)} días con datos") # ── Load proposed actions (to get parameter values) ────────────────────── action_params: dict = {} # campaign_name → parameter try: all_actions = baserow._get_rows(config.BASEROW_TABLE_ACTIONS, { "filter__proposed_at__equal": run_date, }) for a in all_actions: cname = a.get("campaign_name", "") param = a.get("parameter") if cname and param: action_params[cname] = float(param) print(f" ✓ {len(action_params)} parámetros de acción cargados") except Exception as e: print(f" Aviso: no se pudieron cargar parámetros de acción: {e}") # ── Load snapshots from Baserow ─────────────────────────────────────────── print(f"Cargando snapshots de Baserow para {run_date}...") snapshots = baserow.get_snapshots_for_date(run_date) print(f" ✓ {len(snapshots)} snapshots encontrados") if not snapshots: print("ERROR: No hay snapshots en Baserow para esta fecha. " "Ejecuta run.py primero.") return # ── Reconstruct data structures ─────────────────────────────────────────── campaign_details: dict = {} actions: list = [] verticals: dict = {} metrics_all: dict = {} for snap in snapshots: cid = snap.get("campaign_id") or snap.get("campaign_name", "") name = snap["campaign_name"] vertical = snap.get("vertical") or _extract_vertical(name) margin = float(snap.get("margin") or 0) spend = float(snap.get("spend") or 0) leads = int(snap.get("leads") or 0) cpl = float(snap.get("cpl") or 0) action_type = snap.get("action_type") or "MAINTAIN" try: adsets = json.loads(snap.get("adsets_json") or "[]") except Exception: adsets = [] try: ads = json.loads(snap.get("ads_json") or "[]") except Exception: ads = [] campaign_details[cid] = { "name": name, "vertical": vertical, "margin": margin, "adsets": adsets, "ads": ads, "bid_config": {}, } metrics_all[cid] = {"name": name, "spend": spend, "leads": leads, "cpl": cpl} if action_type != "MAINTAIN": actions.append({ "campaign_name": name, "action_type": action_type, "justification": snap.get("justification") or "", "advice": "", "alert": "", "confidence": 0.8, "cpl": cpl, "parameter": action_params.get(name, 1.0), "row_id": snap["id"], }) max_cpl = vertical_cpls.get(vertical, config.META_TARGET_CPL) if vertical not in verticals: verticals[vertical] = {"spend": 0.0, "leads": 0, "margin": 0.0, "target_cpl": max_cpl} verticals[vertical]["spend"] += spend verticals[vertical]["leads"] += leads verticals[vertical]["margin"] += margin # ── Best / worst ────────────────────────────────────────────────────────── with_leads = [m for m in metrics_all.values() if m["leads"] > 0] best_10 = sorted(with_leads, key=lambda x: x["cpl"])[:10] worst_10 = sorted( list(metrics_all.values()), key=lambda x: (x["leads"] > 0, -x["cpl"] if x["leads"] > 0 else 0), )[:10] # ── Send ────────────────────────────────────────────────────────────────── print("Enviando a Slack...") ts = slack_notifier.send_daily_report( daily_totals=daily_totals, best_campaigns=best_10, worst_campaigns=worst_10, actions=actions, target_cpl=config.META_TARGET_CPL, campaigns_analyzed=len(snapshots), mode="DRY_RUN", verticals=verticals, campaign_details=campaign_details, monthly_verticals=monthly_verticals, ) if ts: print(f" ✓ Mensaje enviado (ts={ts})") else: print(" ✗ Error al enviar (revisa token y canal)") if __name__ == "__main__": main()