"""Re-send a day's Slack report from Baserow snapshots (sin llamar a Meta por campaña).""" import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", line_buffering=True) import json from datetime import datetime import config from meta_ads_client import MetaAdsClient from airtable_client import AirtableClient, extract_cursoid from baserow_client import BaserowClient import slack_notifier def main(): run_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d") print(f"Reenviando informe para {run_date}...") meta = MetaAdsClient() baserow = BaserowClient() airtable = AirtableClient() ppl_lookup, _, familia_lookup = airtable.build_campaign_lookups(as_of_date=run_date) # ── Monthly daily totals (fresh de Meta, no se persisten por campaña) ────── print("Obteniendo datos mensuales de Meta...") month_start = f"{run_date[:7]}-01" daily_rows = meta.get_daily_campaign_rows(month_start, run_date) _daily: dict = {} monthly_familias: dict = {} for row in daily_rows: cursoid = extract_cursoid(row["campaign_name"]) or "" familia = familia_lookup.get(cursoid, "Sin familia") ppl = ppl_lookup.get(cursoid, 0) margin = round(row["leads"] * ppl - row["spend"], 2) d = _daily.setdefault(row["date"], {"spend": 0.0, "leads": 0, "margin": 0.0, "f_margins": {}}) d["spend"] += row["spend"] d["leads"] += row["leads"] d["margin"] += margin d["f_margins"][familia] = d["f_margins"].get(familia, 0.0) + margin mf = monthly_familias.setdefault(familia, {"spend": 0.0, "leads": 0, "margin": 0.0}) mf["spend"] += row["spend"] mf["leads"] += row["leads"] mf["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), "f_margins": {f: round(m, 0) for f, m in d["f_margins"].items()}, } 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 ─────────────────────────────────────────── # Nota: urgencia/leads_mes/capping no se persisten en daily_snapshots, así # que al reenviar desde snapshots esos campos salen con su valor por # defecto (slack_notifier ya los trata con .get(...)). campaign_details: dict = {} actions: list = [] familias: dict = {} metrics_all: dict = {} for snap in snapshots: cid = snap.get("campaign_id") or snap.get("campaign_name", "") name = snap["campaign_name"] familia = snap.get("familia") or familia_lookup.get(extract_cursoid(name) or "", "Sin familia") 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, "familia": familia, "margin": margin, "spend_1d": spend, "leads_1d": leads, "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, "parameter": action_params.get(name, 1.0), "row_id": snap["id"], }) f = familias.setdefault(familia, {"spend": 0.0, "leads": 0, "margin": 0.0}) f["spend"] += spend f["leads"] += leads f["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, campaigns_analyzed=len(snapshots), mode="DRY_RUN", familias=familias, campaign_details=campaign_details, monthly_familias=monthly_familias, ) if ts: print(f" ✓ Mensaje enviado (ts={ts})") else: print(" ✗ Error al enviar (revisa token y canal)") if __name__ == "__main__": main()