import json import requests import config from datetime import datetime ALERT_LOSS_EUR = 200 # pérdida absoluta > 200€ → alerta ALERT_MARGIN_PCT = -50 # margen % < -50% → alerta TOP_N = 5 # campañas a mostrar en rankings def _parse_metricas(metricas_json: str) -> dict: try: return json.loads(metricas_json) if metricas_json else {} except (json.JSONDecodeError, TypeError): return {} def _last_n_days_sum(md: dict, n: int) -> dict: days = sorted(md.keys())[-n:] return { "coste": round(sum(md[d].get("coste", 0) for d in days), 2), "ingreso": round(sum(md[d].get("ingreso", 0) for d in days), 2), "margen": round(sum(md[d].get("margen", 0) for d in days), 2), "n_days": len(days), } def _fmt_eur(v: float) -> str: sign = "+" if v > 0 else "" return f"{sign}{v:,.0f}€".replace(",", ".") def _curso(name: str, max_len: int = 42) -> str: return name[:max_len] + ("…" if len(name) > max_len else "") def build_and_send(collected: list, dry_run: bool) -> None: if not config.SLACK_WEBHOOK_URL: print(" ⚠️ SLACK_WEBHOOK_URL no configurada, omitiendo envío.") return now = datetime.now() fco = [item for item in collected if item["campaign"]["curso"].lower().startswith("fco_")] # ── Totales del mes ────────────────────────────────────────────────────── inv_total = round(sum(item["metrics"].get("cost", 0) for item in fco), 2) ing_sumatorio = round(sum( sum(d.get("ingreso", 0) for d in _parse_metricas(item["campaign"].get("metricas_diarias", "{}")).values()) for item in fco ), 2) ing_leads_ppl = round(sum(item["analysis"]["revenue_estimado"] for item in fco), 2) margen_sumatorio = round(ing_sumatorio - inv_total, 2) margen_leads_ppl = round(ing_leads_ppl - inv_total, 2) pct_sumatorio = round(margen_sumatorio / ing_sumatorio * 100, 1) if ing_sumatorio > 0 else 0.0 pct_leads_ppl = round(margen_leads_ppl / ing_leads_ppl * 100, 1) if ing_leads_ppl > 0 else 0.0 # ── Últimos 5 días (desde MetricasDiarias) ─────────────────────────────── last5_rows = [] for item in fco: md = _parse_metricas(item["campaign"].get("metricas_diarias", "{}")) s = _last_n_days_sum(md, 5) if s["n_days"] == 0: continue last5_rows.append({ "curso": item["campaign"]["curso"], "margen": s["margen"], "ingreso": s["ingreso"], "coste": s["coste"], }) last5_rows.sort(key=lambda x: x["margen"]) worst_last5 = last5_rows[:TOP_N] best_last5 = list(reversed(last5_rows[-TOP_N:])) # ── Mes en curso ───────────────────────────────────────────────────────── month_rows = [] for item in fco: cost = item["metrics"].get("cost", 0) rev = item["analysis"]["revenue_estimado"] loss = round(rev - cost, 2) pct = round(item["analysis"]["margen"] * 100, 1) month_rows.append({ "curso": item["campaign"]["curso"], "margen": loss, "margen_pct": pct, "ingreso": round(rev, 2), "coste": round(cost, 2), }) month_rows.sort(key=lambda x: x["margen"]) worst_month = month_rows[:TOP_N] best_month = list(reversed(month_rows[-TOP_N:])) # ── Alertas ────────────────────────────────────────────────────────────── alerts = [ r for r in month_rows if r["margen"] < -ALERT_LOSS_EUR or r["margen_pct"] < ALERT_MARGIN_PCT ] alerts.sort(key=lambda x: x["margen"]) # ── Construir bloques ───────────────────────────────────────────────────── mode = "🔵 DRY RUN" if dry_run else "⚡ PRODUCCIÓN" blocks = [ { "type": "header", "text": {"type": "plain_text", "text": f"LEADS OPTIMIZER — {now.strftime('%d/%m/%Y %H:%M')} {mode}"}, }, { "type": "section", "text": { "type": "mrkdwn", "text": ( f"📊 *RESUMEN DEL MES*\n" f"Inversión: *{inv_total:,.0f}€*\n" f"Ingreso por sumatorio: *{ing_sumatorio:,.0f}€* | Margen por sumatorio: *{_fmt_eur(margen_sumatorio)}* ({pct_sumatorio}%)\n" f"Ingreso LeadsxPPL: *{ing_leads_ppl:,.0f}€* | Margen por LeadsxPPL: *{_fmt_eur(margen_leads_ppl)}* ({pct_leads_ppl}%)" ).replace(",", "."), }, }, ] if alerts: alert_lines = "\n".join( f" 🔴 `{_curso(a['curso'])}` → Pérdida *{_fmt_eur(a['margen'])}* ({a['margen_pct']}%)" for a in alerts ) blocks.append({"type": "divider"}) blocks.append({ "type": "section", "text": { "type": "mrkdwn", "text": f"🚨 *ALERTAS — CAMPAÑAS CON PÉRDIDAS IMPORTANTES*\n{alert_lines}", }, }) blocks.append({"type": "divider"}) def _ranking_text(rows, label, show_pct=False): lines = [] for i, r in enumerate(rows, 1): pct_str = f" ({r['margen_pct']}%)" if show_pct else "" lines.append(f" {i}. `{_curso(r['curso'])}` → *{_fmt_eur(r['margen'])}*{pct_str}") return f"{label}\n" + "\n".join(lines) blocks.append({ "type": "section", "fields": [ { "type": "mrkdwn", "text": _ranking_text(worst_last5, "📉 *PEOR — ÚLTIMOS 5 DÍAS*"), }, { "type": "mrkdwn", "text": _ranking_text(best_last5, "📈 *MEJOR — ÚLTIMOS 5 DÍAS*"), }, ], }) blocks.append({"type": "divider"}) blocks.append({ "type": "section", "fields": [ { "type": "mrkdwn", "text": _ranking_text(worst_month, "📉 *PEOR — MES EN CURSO*", show_pct=True), }, { "type": "mrkdwn", "text": _ranking_text(best_month, "📈 *MEJOR — MES EN CURSO*", show_pct=True), }, ], }) payload = {"blocks": blocks} try: resp = requests.post(config.SLACK_WEBHOOK_URL, json=payload, timeout=10) if resp.status_code != 200: print(f" ⚠️ Slack respondió {resp.status_code}: {resp.text[:200]}") except Exception as e: print(f" ⚠️ Error enviando a Slack: {e}")