- Track v_margins dict per day in daily_totals (one entry per vertical) - Monthly table now shows one margin column per vertical instead of aggregate Margen - Verticals ordered by monthly margin (best first) - Remove separate vertical breakdown section (now redundant) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
7.4 KiB
Python
183 lines
7.4 KiB
Python
"""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, "v_margins": {}})
|
|
d["spend"] += row["spend"]
|
|
d["leads"] += row["leads"]
|
|
d["margin"] += margin
|
|
d["v_margins"][v] = d["v_margins"].get(v, 0.0) + 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),
|
|
"v_margins": {v: round(m, 0) for v, m in d["v_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 ───────────────────────────────────────────
|
|
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()
|