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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
247
scripts/workers/update_campaign_ctas.py
Normal file
247
scripts/workers/update_campaign_ctas.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue