"""Re-send a day's Slack report (tabla diaria/resumen por curso frescos de Meta+Airtable; tarjetas por campaña reconstruidas desde snapshots de Baserow).""" 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, cap_lookup, familia_lookup = airtable.build_campaign_lookups(as_of_date=run_date) # ── Monthly daily totals: Leads Meta vs Leads Airtable (fresco, no se persiste) ─ print("Obteniendo datos mensuales de Meta y Airtable...") month_start = f"{run_date[:7]}-01" daily_rows = meta.get_daily_campaign_rows(month_start, run_date) daily_at_leads = airtable.get_meta_leads_bulk(month_start, run_date) _daily: dict = {} for row in daily_rows: cursoid = extract_cursoid(row["campaign_name"]) or "" ppl = ppl_lookup.get(cursoid, 0) d = _daily.setdefault(row["date"], { "spend": 0.0, "leads_meta": 0, "leads_at": 0, "ing_meta": 0.0, "ing_at": 0.0, }) d["spend"] += row["spend"] d["leads_meta"] += row["leads"] d["ing_meta"] += row["leads"] * ppl for lead in daily_at_leads: ppl = ppl_lookup.get(lead["cursoid"], 0) d = _daily.setdefault(lead["date"], { "spend": 0.0, "leads_meta": 0, "leads_at": 0, "ing_meta": 0.0, "ing_at": 0.0, }) d["leads_at"] += 1 d["ing_at"] += ppl daily_totals = [ { "date": date, "spend": round(d["spend"], 2), "leads_meta": int(d["leads_meta"]), "leads_at": int(d["leads_at"]), "ing_meta": round(d["ing_meta"], 2), "ing_at": round(d["ing_at"], 2), "margin": round(d["ing_meta"] - d["spend"], 2), "margin_pct": round((d["ing_meta"] - d["spend"]) / d["ing_meta"] * 100, 1) if d["ing_meta"] > 0 else 0.0, } for date, d in sorted(_daily.items()) ] print(f" ✓ {len(daily_totals)} días con datos") # ── Resumen y contraste por curso (mismo cálculo que run.py) ──────────────── monthly_metrics_meta = meta.get_campaign_metrics(month_start, run_date) name_by_cid = {} for row in meta.get_all_campaigns(): name_by_cid[row["id"]] = row["name"] def _new_curso_entry(cid_: str) -> dict: return { "campaigns": [], "familia": familia_lookup.get(cid_, "Sin familia"), "ppl": ppl_lookup.get(cid_, 0), "spend": 0.0, "leads_meta": 0, "leads_at_leadform": 0, "leads_at_landing": 0, } curso_summary: dict = {} for mcid, m in monthly_metrics_meta.items(): name = name_by_cid.get(mcid, mcid) cursoid = extract_cursoid(name) or "" if not cursoid: continue cs = curso_summary.setdefault(cursoid, _new_curso_entry(cursoid)) cs["campaigns"].append(name) cs["spend"] += m.get("spend", 0.0) cs["leads_meta"] += m.get("leads", 0) for lead in daily_at_leads: cs = curso_summary.setdefault(lead["cursoid"], _new_curso_entry(lead["cursoid"])) if lead["utm_source"] == "Lead ads": cs["leads_at_leadform"] += 1 else: cs["leads_at_landing"] += 1 for cursoid, cs in curso_summary.items(): leads_at_total = cs["leads_at_leadform"] + cs["leads_at_landing"] cs["leads_at_total"] = leads_at_total cs["cpl_meta"] = round(cs["spend"] / cs["leads_meta"], 2) if cs["leads_meta"] > 0 else 0.0 cs["cpl_at"] = round(cs["spend"] / leads_at_total, 2) if leads_at_total > 0 else 0.0 cs["discrepancia"] = cs["leads_meta"] - leads_at_total # ── 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 = [] 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) 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": {}, } 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"], }) # ── Send (sin diagnóstico estratégico: reenviar no vuelve a llamar a Claude) ─ print("Enviando a Slack...") ts = slack_notifier.send_daily_report( daily_totals=daily_totals, best_campaigns=[], worst_campaigns=[], actions=actions, campaigns_analyzed=len(snapshots), mode="DRY_RUN", campaign_details=campaign_details, curso_summary=curso_summary, portfolio_analysis_text=None, ) if ts: print(f" ✓ Mensaje enviado (ts={ts})") else: print(" ✗ Error al enviar (revisa token y canal)") if __name__ == "__main__": main()