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>
247 lines
8.2 KiB
Python
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 in 4.
|
|
Start your Canadian carrier setup today — 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()
|