new-site/scripts/workers/update_campaign_ctas.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

247 lines
8.2 KiB
Python

"""Update Listmonk campaign CTAs with graduated calls-to-action and UTM tracking.
Instead of every email pushing to the order form, we use a graduated approach:
- Early emails: educational links to service page
- Middle emails: invite to live chat (Tawk.to)
- Late emails: discount code + order form
Also adds UTM parameters for tracking.
"""
import json
import re
import subprocess
import sys
LISTMONK = "http://localhost:9100"
API_USER = "api"
API_PASS = "6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y"
SERVICE_PAGE = "https://performancewest.net/services/telecom/canada-crtc"
ORDER_PAGE = "https://performancewest.net/order/canada-crtc"
CONTACT_PAGE = "https://performancewest.net/contact"
PHONE = "1-888-411-0383"
# Graduated CTA strategy per campaign
# Format: (campaign_id, new_cta_text, new_cta_url, chat_line)
# chat_line: optional line added above footer inviting to live chat
CAMPAIGN_CTAS = {
# === List 3: General Carriers (scheduled: 15-18) ===
# CRTC 4/7 — M&A / exit angle
15: {
"cta_text": "See how the Canadian structure works \u2192",
"cta_url": SERVICE_PAGE,
"utm": "crtc-4-mna",
"chat_line": True,
},
# CRTC 5/7 — Objection handling
16: {
"cta_text": "Have questions? Let\u2019s chat \u2192",
"cta_url": SERVICE_PAGE,
"utm": "crtc-5-objections",
"chat_line": True,
},
# CRTC 6/7 — Process walkthrough
17: {
"cta_text": "See the full setup process \u2192",
"cta_url": SERVICE_PAGE,
"utm": "crtc-6-process",
"chat_line": True,
},
# CRTC 7/7 — Final close
18: {
"cta_text": "Start your Canadian carrier setup \u2192",
"cta_url": ORDER_PAGE,
"utm": "crtc-7-close",
"chat_line": True,
"discount_line": True,
},
# === List 6: ISP / Broadband (scheduled: 9-12) ===
# ISP 4/7 — Canadian revenue opportunity
9: {
"cta_text": "See how Canadian registration works \u2192",
"cta_url": SERVICE_PAGE,
"utm": "isp-4-revenue",
"chat_line": True,
},
# ISP 5/7 — M&A / exit angle
10: {
"cta_text": "See the Canadian carrier structure \u2192",
"cta_url": SERVICE_PAGE,
"utm": "isp-5-mna",
"chat_line": True,
},
# ISP 6/7 — Process walkthrough
11: {
"cta_text": "See what\u2019s involved \u2192",
"cta_url": SERVICE_PAGE,
"utm": "isp-6-process",
"chat_line": True,
},
# ISP 7/7 — FCC enforcement / final close
12: {
"cta_text": "Start your Canadian carrier setup \u2192",
"cta_url": ORDER_PAGE,
"utm": "isp-7-close",
"chat_line": True,
"discount_line": True,
},
# === List 4: Outside Counsel (scheduled: 22-23) ===
# COUNSEL 4/5 — Setup and compliance overview
22: {
"cta_text": "Review the full setup process \u2192",
"cta_url": SERVICE_PAGE,
"utm": "counsel-4-overview",
"chat_line": True,
},
# COUNSEL 5/5 — Referral arrangement
23: {
"cta_text": "Discuss a referral arrangement \u2192",
"cta_url": CONTACT_PAGE,
"utm": "counsel-5-referral",
"chat_line": True,
},
}
# Chat invitation block (inserted before footer)
CHAT_BLOCK = f"""
<tr><td style="padding:20px 40px 10px 40px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%"
style="background:#f0f4f8;border-radius:8px;border:1px solid #e2e8f0;">
<tr><td style="padding:16px 20px;font-family:Arial,sans-serif;font-size:14px;color:#475569;line-height:1.5;">
<strong style="color:#1e3a5f;">Questions? We're online.</strong><br>
Chat with us live on <a href="{SERVICE_PAGE}" style="color:#e63f2a;text-decoration:none;">our website</a>
(look for the chat icon in the bottom-right corner),
or call <strong>{PHONE}</strong>.
</td></tr>
</table>
</td></tr>
"""
DISCOUNT_BLOCK = """
<tr><td style="padding:10px 40px 0 40px;">
<table cellpadding="0" cellspacing="0" border="0" width="100%"
style="background:#eff6ff;border-radius:8px;border:1px solid #bfdbfe;">
<tr><td style="padding:16px 20px;font-family:Arial,sans-serif;font-size:14px;color:#1e3a5f;line-height:1.5;">
<strong>Split it into 4 payments:</strong> Pay ~$975/month with Klarna Pay&nbsp;in&nbsp;4.
Start your Canadian carrier setup today &mdash; pay over time.
</td></tr>
</table>
</td></tr>
"""
def curl_get(path):
cmd = ["curl", "-s", "-u", f"{API_USER}:{API_PASS}", f"{LISTMONK}{path}"]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return json.loads(r.stdout)
def curl_put(path, data):
cmd = ["curl", "-s", "-X", "PUT", "-u", f"{API_USER}:{API_PASS}",
"-H", "Content-Type: application/json",
"-d", json.dumps(data), f"{LISTMONK}{path}"]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return json.loads(r.stdout) if r.stdout else {"error": r.stderr}
def update_campaign(campaign_id, config):
"""Update a campaign's CTA button URL/text and add chat block."""
# Get current campaign
result = curl_get(f"/api/campaigns/{campaign_id}")
campaign = result.get("data", {})
if not campaign:
print(f" Campaign {campaign_id} not found")
return False
name = campaign["name"]
body = campaign.get("body", "")
status = campaign.get("status", "?")
if status not in ("draft", "scheduled"):
print(f" SKIP {campaign_id} ({name}) — status={status}, already sent")
return False
# Build UTM URL
base_url = config["cta_url"]
utm = config["utm"]
separator = "&" if "?" in base_url else "?"
new_url = f"{base_url}{separator}utm_source=listmonk&utm_medium=email&utm_campaign={utm}"
# Replace CTA button URL
old_url = "https://performancewest.net/order/canada-crtc"
body = body.replace(old_url, new_url)
# Replace CTA button text
new_text = config["cta_text"]
# Match common CTA patterns
for pattern in [
r"(>)\s*Start your Canadian carrier setup[^<]*(</a>)",
r"(>)\s*See what's included[^<]*(</a>)",
r"(>)\s*See the full setup[^<]*(</a>)",
r"(>)\s*See the full structure[^<]*(</a>)",
r"(>)\s*See the Canadian carrier structure[^<]*(</a>)",
r"(>)\s*Get started[^<]*(</a>)",
]:
body = re.sub(pattern, f"\\1{new_text}\\2", body, flags=re.IGNORECASE)
# Add chat block before footer (find the footer section)
if config.get("chat_line"):
# Insert before the footer divider or the unsubscribe section
footer_markers = [
"<!-- footer -->",
'<td style="padding:30px 40px;background:#f7f7f7',
'<td style="background:#f7f7f7',
"{{ .UnsubscribeURL }}",
]
for marker in footer_markers:
if marker in body:
insert_content = CHAT_BLOCK
if config.get("discount_line"):
insert_content = DISCOUNT_BLOCK + insert_content
body = body.replace(marker, insert_content + marker, 1)
break
# Update campaign
update_data = {"body": body}
result = curl_put(f"/api/campaigns/{campaign_id}", update_data)
if "error" not in result:
print(f" OK {campaign_id} | {name}")
print(f" CTA: {new_text}")
print(f" URL: {new_url[:80]}")
if config.get("chat_line"):
print(f" + Chat invitation block")
if config.get("discount_line"):
print(f" + Discount code CRTC10")
return True
else:
print(f" FAIL {campaign_id} | {result}")
return False
def main():
print("Updating Listmonk campaign CTAs...")
print(f" Service page: {SERVICE_PAGE}")
print(f" Order page: {ORDER_PAGE}")
print(f" Phone: {PHONE}")
print()
ok = 0
fail = 0
skip = 0
for campaign_id, config in sorted(CAMPAIGN_CTAS.items()):
result = update_campaign(campaign_id, config)
if result is True:
ok += 1
elif result is False:
fail += 1
else:
skip += 1
print(f"\nDone: {ok} updated, {fail} failed/skipped")
if __name__ == "__main__":
main()