From 4848253c495638b32539cc354ec38cf2dffd5188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20G=C3=B3mez?= Date: Thu, 11 Jun 2026 23:04:40 +0200 Subject: [PATCH] Fix leadform double-attribution and Slack message truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - airtable_client: restrict leadform cursoid path (path 5) to PMX-only campaigns, preventing triple-attribution to search+pmx+leadform companion - slack_reporter: derive margen from ingreso-coste instead of stale MetricasDiarias field; split monthly rankings from fields to separate section blocks; add _trunc() helper to clamp all text blocks under Slack's 3000-char limit - agent: increase portfolio_daily_analysis max_tokens 500→800 - backfill: extend to June 11 Co-Authored-By: Claude Sonnet 4.6 --- agent.py | 2 +- airtable_client.py | 15 ++++++++----- backfill_leadform_jun8_10.py | 2 +- slack_reporter.py | 41 +++++++++++++++++++++--------------- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/agent.py b/agent.py index d64a272..f2c97e5 100644 --- a/agent.py +++ b/agent.py @@ -141,7 +141,7 @@ def portfolio_daily_analysis(collected: list) -> str: try: response = client.messages.create( model="claude-sonnet-4-6", - max_tokens=500, + max_tokens=800, system=PORTFOLIO_SYSTEM, messages=[{ "role": "user", diff --git a/airtable_client.py b/airtable_client.py index e0d4593..e0984e9 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -359,7 +359,10 @@ class AirtableClient: m = re.search(r'fco_(?:search|pmx)_(\d+)', campaign_name, re.IGNORECASE) if m: course_num = m.group(1) - if "pmx" in campaign_name.lower() and "_leadform" not in campaign_name.lower(): + is_pmx_non_leadform = ( + "pmx" in campaign_name.lower() and "_leadform" not in campaign_name.lower() + ) + if is_pmx_non_leadform: utm_source = "pmx" elif "search" in campaign_name.lower(): utm_source = "google" @@ -370,10 +373,12 @@ class AirtableClient: f"AND({{attr_cursoid}}='{course_num}'," f"{{attr_utm_source}}='{utm_source}')" ) - leadform_cursoid_clause = ( - f"AND({{attr_cursoid}}='{course_num}'," - f"{{UserAgent del visitante}}='Google-Ads-Notifications')" - ) + # Path 5: leadform leads en Airtable, solo para PMX (no search, no _leadform) + if is_pmx_non_leadform: + leadform_cursoid_clause = ( + f"AND({{attr_cursoid}}='{course_num}'," + f"{{UserAgent del visitante}}='Google-Ads-Notifications')" + ) formula = ( f"AND(" diff --git a/backfill_leadform_jun8_10.py b/backfill_leadform_jun8_10.py index a31d7bd..7371a98 100644 --- a/backfill_leadform_jun8_10.py +++ b/backfill_leadform_jun8_10.py @@ -8,7 +8,7 @@ import re from datetime import datetime from airtable_client import AirtableClient -DAYS = ["2026-06-08", "2026-06-09", "2026-06-10"] +DAYS = ["2026-06-08", "2026-06-09", "2026-06-10", "2026-06-11"] def _course_num(name: str) -> str | None: diff --git a/slack_reporter.py b/slack_reporter.py index b89c6fe..87f3477 100644 --- a/slack_reporter.py +++ b/slack_reporter.py @@ -46,14 +46,20 @@ def _last_n_days_combined(current_md: dict, prev_md: dict, now: datetime, n: int entries.sort(key=lambda x: x[0]) last_n = entries[-n:] + total_coste = round(sum(v.get("coste", 0) for _, v in last_n), 2) + total_ingreso = round(sum(v.get("ingreso", 0) for _, v in last_n), 2) return { - "coste": round(sum(v.get("coste", 0) for _, v in last_n), 2), - "ingreso": round(sum(v.get("ingreso", 0) for _, v in last_n), 2), - "margen": round(sum(v.get("margen", 0) for _, v in last_n), 2), + "coste": total_coste, + "ingreso": total_ingreso, + "margen": round(total_ingreso - total_coste, 2), "n_days": len(last_n), } +def _trunc(text: str, limit: int = 2950) -> str: + return text if len(text) <= limit else text[:limit - 3] + "…" + + def _fmt_eur(v: float) -> str: sign = "+" if v > 0 else "" return f"{sign}{v:,.0f}€".replace(",", ".") @@ -195,7 +201,7 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N "type": "section", "text": { "type": "mrkdwn", - "text": f"📅 *MÁRGENES POR DÍA — {MESES_ES[now.month].upper()}*\n```\n" + "\n".join(rows) + "\n```", + "text": _trunc(f"📅 *MÁRGENES POR DÍA — {MESES_ES[now.month].upper()}*\n```\n" + "\n".join(rows) + "\n```"), }, } @@ -289,14 +295,14 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N "type": "section", "text": { "type": "mrkdwn", - "text": _ranking_last5_text(worst_last5, "📉 *PEOR — ÚLTIMOS 5 DÍAS*"), + "text": _trunc(_ranking_last5_text(worst_last5, "📉 *PEOR — ÚLTIMOS 5 DÍAS*")), }, }) blocks.append({ "type": "section", "text": { "type": "mrkdwn", - "text": _ranking_last5_text(best_last5, "📈 *MEJOR — ÚLTIMOS 5 DÍAS*"), + "text": _trunc(_ranking_last5_text(best_last5, "📈 *MEJOR — ÚLTIMOS 5 DÍAS*")), }, }) @@ -304,16 +310,17 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N blocks.append({"type": "divider"}) blocks.append({ "type": "section", - "fields": [ - { - "type": "mrkdwn", - "text": _ranking_month_text(worst_month, "📉 *PEOR — MES EN CURSO*"), - }, - { - "type": "mrkdwn", - "text": _ranking_month_text(best_month, "📈 *MEJOR — MES EN CURSO*"), - }, - ], + "text": { + "type": "mrkdwn", + "text": _trunc(_ranking_month_text(worst_month, "📉 *PEOR — MES EN CURSO*")), + }, + }) + blocks.append({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": _trunc(_ranking_month_text(best_month, "📈 *MEJOR — MES EN CURSO*")), + }, }) if portfolio_analysis_text: @@ -322,7 +329,7 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N "type": "section", "text": { "type": "mrkdwn", - "text": f"🤖 *DIAGNÓSTICO ESTRATÉGICO*\n{portfolio_analysis_text}", + "text": _trunc(f"🤖 *DIAGNÓSTICO ESTRATÉGICO*\n{portfolio_analysis_text}"), }, })