Simplify Slack report: vertical-first hierarchical structure
Replace one-message-per-campaign with: - Message 1: monthly table + vertical scorecard + actions + todo-en-orden - One message per vertical that has non-MAINTAIN actions or ad PAUSEs - Verticals with no issues are summarised in a single line in message 1 - Remove top-10 best/worst lists (noise) - Adset detail (top 3) only shown for non-MAINTAIN campaigns - Recommendations and approve/reject buttons are now front and centre Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
17f80842be
commit
3afc1c25c1
@ -157,6 +157,16 @@ def _adset_ad_table(items: list, label: str, show_bid: bool = False, show_7d: bo
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _vertical_status_emoji(v_cpl: float, v_obj: float, has_action: bool) -> str:
|
||||||
|
if v_cpl == 0 or v_obj == 0:
|
||||||
|
return "⚪"
|
||||||
|
if v_cpl <= v_obj:
|
||||||
|
return "⚠️" if has_action else "✅"
|
||||||
|
if v_cpl <= v_obj * 1.3:
|
||||||
|
return "⚠️"
|
||||||
|
return "❌"
|
||||||
|
|
||||||
|
|
||||||
def send_daily_report(
|
def send_daily_report(
|
||||||
daily_totals: list,
|
daily_totals: list,
|
||||||
best_campaigns: list,
|
best_campaigns: list,
|
||||||
@ -175,8 +185,34 @@ def send_daily_report(
|
|||||||
month_name = now.strftime("%B %Y").capitalize()
|
month_name = now.strftime("%B %Y").capitalize()
|
||||||
prefix = config.META_CAMPAIGN_PREFIX
|
prefix = config.META_CAMPAIGN_PREFIX
|
||||||
mode_label = "DRY RUN" if mode == "DRY_RUN" else "PRODUCCIÓN"
|
mode_label = "DRY RUN" if mode == "DRY_RUN" else "PRODUCCIÓN"
|
||||||
target_str = f"{target_cpl:.2f}€" if target_cpl > 0 else "—"
|
|
||||||
|
|
||||||
|
action_map = {a["campaign_name"]: a for a in actions}
|
||||||
|
details_map = campaign_details or {}
|
||||||
|
name_to_cid = {d["name"]: cid for cid, d in details_map.items()}
|
||||||
|
|
||||||
|
# ── Classify campaigns: issues vs OK ─────────────────────────────────────
|
||||||
|
camps_issues: dict = {} # name → (cid, detail, action_or_None)
|
||||||
|
camps_ok: dict = {} # name → (cid, detail)
|
||||||
|
|
||||||
|
for cid, detail in details_map.items():
|
||||||
|
name = detail["name"]
|
||||||
|
act = action_map.get(name)
|
||||||
|
has_action = bool(act and act["action_type"] != "MAINTAIN")
|
||||||
|
has_ad_pause = any(
|
||||||
|
ad.get("accion") == "PAUSE" and ad.get("row_id")
|
||||||
|
for ad in detail.get("ads", [])
|
||||||
|
)
|
||||||
|
if has_action or has_ad_pause:
|
||||||
|
camps_issues[name] = (cid, detail, act)
|
||||||
|
else:
|
||||||
|
camps_ok[name] = (cid, detail)
|
||||||
|
|
||||||
|
# Group issues by vertical
|
||||||
|
by_vertical_issues: dict = {}
|
||||||
|
for name, (cid, detail, act) in camps_issues.items():
|
||||||
|
by_vertical_issues.setdefault(detail["vertical"], []).append((cid, detail, act))
|
||||||
|
|
||||||
|
# ── Message 1: Executive dashboard ───────────────────────────────────────
|
||||||
blocks: list = [
|
blocks: list = [
|
||||||
{
|
{
|
||||||
"type": "header",
|
"type": "header",
|
||||||
@ -185,26 +221,21 @@ def send_daily_report(
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── Rentabilidad mensual con margen por vertical ─────────────────────────
|
# Monthly profitability table
|
||||||
if daily_totals:
|
if daily_totals:
|
||||||
# Orden de verticales: por margen mensual acumulado (mejor primero)
|
|
||||||
v_order = (
|
v_order = (
|
||||||
sorted(monthly_verticals.keys(), key=lambda v: -monthly_verticals[v]["margin"])
|
sorted(monthly_verticals.keys(), key=lambda v: -monthly_verticals[v]["margin"])
|
||||||
if monthly_verticals else []
|
if monthly_verticals else []
|
||||||
)
|
)
|
||||||
cw = 7 # ancho de cada columna de vertical
|
cw = 7
|
||||||
|
|
||||||
# Cabecera
|
|
||||||
header = f"{'Día':<5} {'Gasto':>6} {'Leads':>5} {'CPL':>7}"
|
header = f"{'Día':<5} {'Gasto':>6} {'Leads':>5} {'CPL':>7}"
|
||||||
for v in v_order:
|
for v in v_order:
|
||||||
header += f" {v[:6]:>{cw}}"
|
header += f" {v[:6]:>{cw}}"
|
||||||
header += " Est"
|
header += " Est"
|
||||||
sep = "─" * len(header)
|
sep = "─" * len(header)
|
||||||
|
|
||||||
lines = [header, sep]
|
lines = [header, sep]
|
||||||
total_spend = total_leads = total_margin = 0.0
|
total_spend = total_leads = total_margin = 0.0
|
||||||
total_v = {v: 0.0 for v in v_order}
|
total_v = {v: 0.0 for v in v_order}
|
||||||
|
|
||||||
for d in daily_totals:
|
for d in daily_totals:
|
||||||
day = d["date"][8:10] + "/" + d["date"][5:7]
|
day = d["date"][8:10] + "/" + d["date"][5:7]
|
||||||
margin = d.get("margin", 0.0)
|
margin = d.get("margin", 0.0)
|
||||||
@ -221,7 +252,6 @@ def send_daily_report(
|
|||||||
row += f" {vm_s:>{cw}}"
|
row += f" {vm_s:>{cw}}"
|
||||||
row += f" {icon}"
|
row += f" {icon}"
|
||||||
lines.append(row)
|
lines.append(row)
|
||||||
|
|
||||||
lines.append(sep)
|
lines.append(sep)
|
||||||
total_row = f"{'TOTAL':<5} {total_spend:>5.0f}€ {int(total_leads):>5} {'':>7}"
|
total_row = f"{'TOTAL':<5} {total_spend:>5.0f}€ {int(total_leads):>5} {'':>7}"
|
||||||
for v in v_order:
|
for v in v_order:
|
||||||
@ -229,7 +259,6 @@ def send_daily_report(
|
|||||||
tv_s = f"+{tv:.0f}€" if tv >= 0 else f"{tv:.0f}€"
|
tv_s = f"+{tv:.0f}€" if tv >= 0 else f"{tv:.0f}€"
|
||||||
total_row += f" {tv_s:>{cw}}"
|
total_row += f" {tv_s:>{cw}}"
|
||||||
lines.append(total_row)
|
lines.append(total_row)
|
||||||
|
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"text": {
|
"text": {
|
||||||
@ -243,11 +272,12 @@ def send_daily_report(
|
|||||||
"text": {"type": "mrkdwn", "text": "_Sin datos del mes en curso aún._"},
|
"text": {"type": "mrkdwn", "text": "_Sin datos del mes en curso aún._"},
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Resumen por vertical ──────────────────────────────────────────────────
|
|
||||||
if verticals:
|
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
lines = [f"{'Vertical':<16} {'Gasto':>7} {'Leads':>5} {'CPL':>7} {'Obj':>7} {'Margen':>9}"]
|
|
||||||
lines.append("─" * 57)
|
# Vertical scorecard
|
||||||
|
if verticals:
|
||||||
|
lines = [f"{'Vertical':<16} {'Gasto':>6} {'Leads':>5} {'CPL':>7} {'Obj':>7} {'Margen':>9}"]
|
||||||
|
lines.append("─" * 58)
|
||||||
for v, data in sorted(verticals.items(), key=lambda x: -x[1]["margin"]):
|
for v, data in sorted(verticals.items(), key=lambda x: -x[1]["margin"]):
|
||||||
v_leads = data["leads"]
|
v_leads = data["leads"]
|
||||||
v_spend = data["spend"]
|
v_spend = data["spend"]
|
||||||
@ -256,51 +286,93 @@ def send_daily_report(
|
|||||||
v_obj = data.get("target_cpl", 0)
|
v_obj = data.get("target_cpl", 0)
|
||||||
m_sign = f"+{v_m:.0f}€" if v_m >= 0 else f"{v_m:.0f}€"
|
m_sign = f"+{v_m:.0f}€" if v_m >= 0 else f"{v_m:.0f}€"
|
||||||
obj_str = f"{v_obj:.2f}€" if v_obj else " —"
|
obj_str = f"{v_obj:.2f}€" if v_obj else " —"
|
||||||
lines.append(f"{v:<16} {v_spend:>6.0f}€ {v_leads:>5} {v_cpl:>6.2f}€ {obj_str:>7} {m_sign:>9}")
|
has_act = v in by_vertical_issues
|
||||||
|
st = _vertical_status_emoji(v_cpl, v_obj, has_act)
|
||||||
|
lines.append(
|
||||||
|
f"{st} {v:<14} {v_spend:>5.0f}€ {v_leads:>5} {v_cpl:>6.2f}€ {obj_str:>7} {m_sign:>9}"
|
||||||
|
)
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"text": {"type": "mrkdwn",
|
"text": {"type": "mrkdwn",
|
||||||
"text": "*Resumen por vertical (ayer)*\n```" + "\n".join(lines) + "```"},
|
"text": "*Verticales · ayer*\n```" + "\n".join(lines) + "```"},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Actions section
|
||||||
|
if actions:
|
||||||
blocks.append({"type": "divider"})
|
blocks.append({"type": "divider"})
|
||||||
|
|
||||||
# ── Top 10 mejores ────────────────────────────────────────────────────────
|
|
||||||
if best_campaigns:
|
|
||||||
lines = []
|
|
||||||
for i, m in enumerate(best_campaigns, 1):
|
|
||||||
lines.append(
|
|
||||||
f"{i}. *{m['name'][:46]}* CPL: {m['cpl']:.2f}€ | {m['leads']} leads | {m['spend']:.0f}€"
|
|
||||||
)
|
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "section",
|
"type": "section",
|
||||||
"text": {"type": "mrkdwn", "text": "*Top 10 mejores (ayer)*\n" + "\n".join(lines)},
|
"text": {"type": "mrkdwn", "text": "*🚨 Acciones recomendadas*"},
|
||||||
})
|
})
|
||||||
blocks.append({"type": "divider"})
|
for act in actions:
|
||||||
|
atype = act["action_type"]
|
||||||
# ── Top 10 peores ─────────────────────────────────────────────────────────
|
emoji, alabel = _ACTION_DISPLAY.get(atype, ("⚪", atype))
|
||||||
if worst_campaigns:
|
cid = name_to_cid.get(act["campaign_name"])
|
||||||
lines = []
|
bid_cfg = details_map.get(cid, {}).get("bid_config", {}) if cid else {}
|
||||||
for i, m in enumerate(worst_campaigns, 1):
|
budget = bid_cfg.get("daily_budget_eur")
|
||||||
if m["leads"] == 0:
|
text = f"{emoji} *{act['campaign_name'][:60]}* → *{alabel}*"
|
||||||
lines.append(f"{i}. *{m['name'][:46]}* 0 leads | {m['spend']:.0f}€ gastado")
|
if act.get("justification"):
|
||||||
else:
|
text += f"\n_{act['justification'][:200]}_"
|
||||||
lines.append(
|
if act.get("alert"):
|
||||||
f"{i}. *{m['name'][:46]}* CPL: {m['cpl']:.2f}€ | {m['leads']} leads | {m['spend']:.0f}€"
|
text += f"\n:warning: {act['alert'][:150]}"
|
||||||
)
|
effect = _effect_text(act, budget)
|
||||||
|
if effect:
|
||||||
|
text += f"\n{effect}"
|
||||||
|
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": text}})
|
||||||
|
if atype in _ACTIONABLE:
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "section",
|
"type": "actions",
|
||||||
"text": {"type": "mrkdwn", "text": "*Top 10 peores (ayer)*\n" + "\n".join(lines)},
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"text": {"type": "plain_text", "text": "✅ Aprobar"},
|
||||||
|
"style": "primary",
|
||||||
|
"value": f"approve:{act['row_id']}",
|
||||||
|
"action_id": f"approve_{act['row_id']}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button",
|
||||||
|
"text": {"type": "plain_text", "text": "❌ Rechazar"},
|
||||||
|
"style": "danger",
|
||||||
|
"value": f"reject:{act['row_id']}",
|
||||||
|
"action_id": f"reject_{act['row_id']}",
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
blocks.append({"type": "divider"})
|
# Ad pause count notice
|
||||||
|
total_ad_pauses = sum(
|
||||||
|
1 for _, detail, _ in camps_issues.values()
|
||||||
|
for ad in detail.get("ads", [])
|
||||||
|
if ad.get("accion") == "PAUSE" and ad.get("row_id")
|
||||||
|
)
|
||||||
|
if total_ad_pauses > 0:
|
||||||
|
noun = "anuncio" if total_ad_pauses == 1 else "anuncios"
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"type": "context",
|
"type": "context",
|
||||||
"elements": [{"type": "mrkdwn",
|
"elements": [{"type": "mrkdwn",
|
||||||
"text": f"{campaigns_analyzed} campañas analizadas — detalle por campaña a continuación"}],
|
"text": f"⛔ {total_ad_pauses} {noun} recomendados para pausar — ver detalle por vertical"}],
|
||||||
|
})
|
||||||
|
|
||||||
|
# "Todo en orden" summary
|
||||||
|
if camps_ok:
|
||||||
|
blocks.append({"type": "divider"})
|
||||||
|
ok_lines = []
|
||||||
|
for name, (_, detail) in sorted(camps_ok.items(), key=lambda x: x[1][1]["vertical"]):
|
||||||
|
v = detail["vertical"]
|
||||||
|
leads_1d = detail.get("leads_1d", 0)
|
||||||
|
spend_1d = detail.get("spend_1d", 0.0)
|
||||||
|
ok_lines.append(f"✅ *{name}* · {spend_1d:.0f}€ / {leads_1d} leads ayer")
|
||||||
|
blocks.append({
|
||||||
|
"type": "section",
|
||||||
|
"text": {"type": "mrkdwn", "text": "*Todo en orden*\n" + "\n".join(ok_lines)},
|
||||||
|
})
|
||||||
|
|
||||||
|
blocks.append({
|
||||||
|
"type": "context",
|
||||||
|
"elements": [{"type": "mrkdwn", "text": f"{campaigns_analyzed} campañas analizadas"}],
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Enviar resumen ────────────────────────────────────────────────────────
|
|
||||||
result = _post(
|
result = _post(
|
||||||
"chat.postMessage",
|
"chat.postMessage",
|
||||||
channel=config.SLACK_CHANNEL_ID,
|
channel=config.SLACK_CHANNEL_ID,
|
||||||
@ -309,91 +381,97 @@ def send_daily_report(
|
|||||||
)
|
)
|
||||||
ts = result.get("ts")
|
ts = result.get("ts")
|
||||||
|
|
||||||
# ── Un mensaje por campaña (sin límite de bloques) ────────────────────────
|
# ── Messages 2-N: one per vertical with issues ────────────────────────────
|
||||||
if campaign_details:
|
for v, camp_list in sorted(by_vertical_issues.items()):
|
||||||
action_map = {a["campaign_name"]: a for a in actions}
|
v_data = (verticals or {}).get(v, {})
|
||||||
|
v_spend = v_data.get("spend", 0)
|
||||||
|
v_leads = v_data.get("leads", 0)
|
||||||
|
v_cpl = round(v_spend / v_leads, 2) if v_leads > 0 else 0.0
|
||||||
|
v_obj = v_data.get("target_cpl", 0)
|
||||||
|
v_margin = v_data.get("margin", 0)
|
||||||
|
m_str = f"+{v_margin:.0f}€" if v_margin >= 0 else f"{v_margin:.0f}€"
|
||||||
|
obj_str = f" · obj {v_obj:.2f}€" if v_obj else ""
|
||||||
|
|
||||||
for cid, detail in campaign_details.items():
|
v_blocks: list = [{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": (
|
||||||
|
f"📊 *{v.upper()}* · {v_spend:.0f}€ · {v_leads} leads · "
|
||||||
|
f"CPL {v_cpl:.2f}€{obj_str} · Margen {m_str}"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
for cid, detail, act in sorted(camp_list, key=lambda x: -x[1].get("spend_1d", 0)):
|
||||||
name = detail["name"]
|
name = detail["name"]
|
||||||
vertical = detail["vertical"]
|
|
||||||
margin = detail["margin"]
|
|
||||||
spend_1d = detail.get("spend_1d", 0.0)
|
spend_1d = detail.get("spend_1d", 0.0)
|
||||||
leads_1d = detail.get("leads_1d", 0)
|
leads_1d = detail.get("leads_1d", 0)
|
||||||
|
margin = detail["margin"]
|
||||||
|
m_str2 = f"+{margin:.2f}€" if margin >= 0 else f"{margin:.2f}€"
|
||||||
adsets = detail.get("adsets", [])
|
adsets = detail.get("adsets", [])
|
||||||
ads = detail.get("ads", [])
|
ads = detail.get("ads", [])
|
||||||
bid_cfg = detail.get("bid_config", {})
|
bid_cfg = detail.get("bid_config", {})
|
||||||
m_str = f"+{margin:.2f}€" if margin >= 0 else f"{margin:.2f}€"
|
|
||||||
action = action_map.get(name)
|
|
||||||
|
|
||||||
strategy = bid_cfg.get("bid_strategy", "")
|
|
||||||
strategy_label = _STRATEGY_LABELS.get(strategy, strategy or "—")
|
|
||||||
budget = bid_cfg.get("daily_budget_eur")
|
budget = bid_cfg.get("daily_budget_eur")
|
||||||
budget_str = f"{budget:.0f}€/día" if budget else "—"
|
strategy = bid_cfg.get("bid_strategy", "")
|
||||||
|
strat_label = _STRATEGY_LABELS.get(strategy, strategy or "—")
|
||||||
|
|
||||||
atype = action["action_type"] if action else "MAINTAIN"
|
atype = act["action_type"] if act else "MAINTAIN"
|
||||||
emoji, alabel = _ACTION_DISPLAY.get(atype, ("⚪", atype))
|
emoji, alabel = _ACTION_DISPLAY.get(atype, ("⚪", atype))
|
||||||
|
|
||||||
header_text = (
|
camp_text = (
|
||||||
f"{emoji} *{name}*\n"
|
f"{emoji} *{name}*\n"
|
||||||
f"Vertical: _{vertical}_ | Ayer: *{spend_1d:.0f}€ / {leads_1d} leads* | "
|
f"Ayer: {spend_1d:.0f}€ / {leads_1d} leads · Margen: {m_str2} · "
|
||||||
f"Margen: *{m_str}* | Estrategia: `{strategy_label}` | Presupuesto: {budget_str}\n"
|
f"`{strat_label}`" + (f" · {budget:.0f}€/día" if budget else "") +
|
||||||
f"*Decisión: {alabel}*"
|
f"\n*{alabel}*"
|
||||||
)
|
)
|
||||||
if action:
|
if act and act.get("justification"):
|
||||||
header_text += f"\n_{action.get('justification', '')[:500]}_"
|
camp_text += f" — _{act['justification'][:160]}_"
|
||||||
if action.get("alert"):
|
if act and act.get("alert"):
|
||||||
header_text += f"\n:warning: {action['alert']}"
|
camp_text += f"\n:warning: {act['alert'][:130]}"
|
||||||
|
|
||||||
camp_blocks: list = [
|
v_blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": camp_text}})
|
||||||
{"type": "section", "text": {"type": "mrkdwn", "text": header_text}},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Botones de acción a nivel campaña
|
# Approve/Reject buttons for campaign action
|
||||||
if action and atype in _ACTIONABLE:
|
if act and atype in _ACTIONABLE:
|
||||||
effect = _effect_text(action, budget)
|
effect = _effect_text(act, budget)
|
||||||
if effect:
|
if effect:
|
||||||
camp_blocks.append({
|
v_blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": effect}})
|
||||||
"type": "section",
|
v_blocks.append({
|
||||||
"text": {"type": "mrkdwn", "text": effect},
|
|
||||||
})
|
|
||||||
camp_blocks.append({
|
|
||||||
"type": "actions",
|
"type": "actions",
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"type": "button",
|
"type": "button",
|
||||||
"text": {"type": "plain_text", "text": "✅ Aprobar"},
|
"text": {"type": "plain_text", "text": "✅ Aprobar"},
|
||||||
"style": "primary",
|
"style": "primary",
|
||||||
"value": f"approve:{action['row_id']}",
|
"value": f"approve:{act['row_id']}",
|
||||||
"action_id": f"approve_{action['row_id']}",
|
"action_id": f"approve_{act['row_id']}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "button",
|
"type": "button",
|
||||||
"text": {"type": "plain_text", "text": "❌ Rechazar"},
|
"text": {"type": "plain_text", "text": "❌ Rechazar"},
|
||||||
"style": "danger",
|
"style": "danger",
|
||||||
"value": f"reject:{action['row_id']}",
|
"value": f"reject:{act['row_id']}",
|
||||||
"action_id": f"reject_{action['row_id']}",
|
"action_id": f"reject_{act['row_id']}",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
# Tabla adsets + anuncios
|
# Adset table — top 3, only for non-MAINTAIN campaigns
|
||||||
adset_table = _adset_ad_table(adsets, "Conjuntos de anuncios (últimos 3 días)", show_bid=True)
|
if atype != "MAINTAIN" and adsets:
|
||||||
ad_table = _adset_ad_table(ads, "Anuncios (últimos 7 días)", show_7d=True)
|
adset_table = _adset_ad_table(adsets[:3], "Conjuntos de anuncios (3 días)", show_bid=True)
|
||||||
combined = "\n\n".join(filter(None, [adset_table, ad_table]))
|
if adset_table:
|
||||||
for chunk in [combined[i:i+2900] for i in range(0, len(combined), 2900)]:
|
for chunk in [adset_table[i:i+2900] for i in range(0, len(adset_table), 2900)]:
|
||||||
camp_blocks.append({
|
v_blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": chunk}})
|
||||||
"type": "section",
|
|
||||||
"text": {"type": "mrkdwn", "text": chunk},
|
|
||||||
})
|
|
||||||
|
|
||||||
# Botones de pausa por anuncio
|
# Ad pause buttons
|
||||||
camp_blocks.extend(_ad_action_blocks(ads))
|
v_blocks.extend(_ad_action_blocks(ads))
|
||||||
|
|
||||||
_post(
|
_post(
|
||||||
"chat.postMessage",
|
"chat.postMessage",
|
||||||
channel=config.SLACK_CHANNEL_ID,
|
channel=config.SLACK_CHANNEL_ID,
|
||||||
blocks=camp_blocks,
|
blocks=v_blocks,
|
||||||
text=name,
|
text=f"Detalle vertical: {v}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return ts
|
return ts
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user