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>
157 lines
8.4 KiB
Python
157 lines
8.4 KiB
Python
"""Add savings comparison table to scheduled Listmonk campaigns."""
|
|
import json
|
|
import subprocess
|
|
|
|
API_USER = "api"
|
|
API_PASS = "6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y"
|
|
LISTMONK = "http://localhost:9100"
|
|
|
|
# HTML savings table for email (inline styles, email-safe)
|
|
SAVINGS_TABLE = """
|
|
<tr><td style="padding:20px 40px;">
|
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="border-radius:8px;overflow:hidden;border:1px solid #e2e8f0;">
|
|
<tr>
|
|
<td style="background:#1a2744;padding:12px 16px;font-family:Arial,sans-serif;font-size:14px;font-weight:bold;color:#ffffff;text-align:center;" colspan="2">
|
|
What you're paying now vs. what you could be paying
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="background:#fef2f2;padding:12px 16px;width:50%;vertical-align:top;border-right:1px solid #e2e8f0;">
|
|
<p style="font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:#991b1b;margin:0 0 8px;">US Carrier (FCC Section 214)</p>
|
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-family:Arial,sans-serif;font-size:11px;color:#7f1d1d;">
|
|
<tr><td style="padding:2px 0;">214 filing + attorney</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$7K-$17K</td></tr>
|
|
<tr><td style="padding:2px 0;">USF contributions (36.6%)</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$12K+/yr</td></tr>
|
|
<tr><td style="padding:2px 0;">CALEA compliance</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$50K-$500K+</td></tr>
|
|
<tr><td style="padding:2px 0;">STIR/SHAKEN</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$3K-$5K/yr</td></tr>
|
|
<tr><td style="padding:2px 0;">State PUC registrations</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$2K-$5K/yr</td></tr>
|
|
<tr><td style="padding:2px 0;">499-A filing + RMD</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$1.5K/yr</td></tr>
|
|
<tr><td style="padding:2px 0;">Customer surcharges</td><td style="padding:2px 0;text-align:right;font-weight:bold;">+15-40%</td></tr>
|
|
<tr><td colspan="2" style="border-top:1px solid #fca5a5;padding:6px 0 0;font-weight:bold;color:#991b1b;">
|
|
Year 1: $58K-$525K+ | Ongoing: $23K+/yr
|
|
</td></tr>
|
|
</table>
|
|
</td>
|
|
<td style="background:#f0fdf4;padding:12px 16px;width:50%;vertical-align:top;">
|
|
<p style="font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:#166534;margin:0 0 8px;">Canadian Carrier (CRTC Registration)</p>
|
|
<table cellpadding="0" cellspacing="0" border="0" width="100%" style="font-family:Arial,sans-serif;font-size:11px;color:#14532d;">
|
|
<tr><td style="padding:2px 0;">CRTC registration</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$3,899</td></tr>
|
|
<tr><td style="padding:2px 0;">USF contributions</td><td style="padding:2px 0;text-align:right;font-weight:bold;color:#166534;">$0</td></tr>
|
|
<tr><td style="padding:2px 0;">CALEA equivalent</td><td style="padding:2px 0;text-align:right;font-weight:bold;color:#166534;">$0</td></tr>
|
|
<tr><td style="padding:2px 0;">STIR/SHAKEN</td><td style="padding:2px 0;text-align:right;font-weight:bold;color:#166534;">$0</td></tr>
|
|
<tr><td style="padding:2px 0;">Provincial registration</td><td style="padding:2px 0;text-align:right;font-weight:bold;color:#166534;">$0</td></tr>
|
|
<tr><td style="padding:2px 0;">Annual maintenance</td><td style="padding:2px 0;text-align:right;font-weight:bold;">$349/yr</td></tr>
|
|
<tr><td style="padding:2px 0;">Customer surcharges</td><td style="padding:2px 0;text-align:right;font-weight:bold;color:#166534;">$0</td></tr>
|
|
<tr><td colspan="2" style="border-top:1px solid #86efac;padding:6px 0 0;font-weight:bold;color:#166534;">
|
|
Year 1: $3,899 | Ongoing: $349/yr
|
|
</td></tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="2" style="background:#dcfce7;padding:12px 16px;text-align:center;">
|
|
<p style="font-family:Arial,sans-serif;font-size:16px;font-weight:bold;color:#166534;margin:0;">
|
|
Save $55,000 - $525,000+ in Year 1
|
|
</p>
|
|
<p style="font-family:Arial,sans-serif;font-size:11px;color:#15803d;margin:4px 0 0;">
|
|
Then ~$23,000/yr ongoing • Same +1 country code • Zero customer surcharges
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td></tr>
|
|
"""
|
|
|
|
def api_get(path):
|
|
r = subprocess.run(["curl", "-s", "-u", f"{API_USER}:{API_PASS}", f"{LISTMONK}{path}"],
|
|
capture_output=True, text=True, timeout=10)
|
|
return json.loads(r.stdout)
|
|
|
|
def api_put(path, data):
|
|
r = subprocess.run(["curl", "-s", "-X", "PUT", "-u", f"{API_USER}:{API_PASS}",
|
|
"-H", "Content-Type: application/json", "-d", json.dumps(data),
|
|
f"{LISTMONK}{path}"], capture_output=True, text=True, timeout=10)
|
|
return json.loads(r.stdout) if r.stdout else {}
|
|
|
|
# Campaigns to update with new subject lines emphasizing savings
|
|
# Only update scheduled (not yet sent) campaigns
|
|
SUBJECT_UPDATES = {
|
|
9: "Your competitors are saving $23K/yr in regulatory costs. Here's how.",
|
|
10: "$58K-$525K in Year 1 compliance costs — or $3,899. The math.",
|
|
11: "CALEA alone costs $50K-$500K. Canadian carriers pay $0.",
|
|
12: "Last email: $55K\u2013$525K in savings \u2014 start at ~$975 with 4 payments",
|
|
15: "The $55,000+ reason 503 US carriers also registered in Canada",
|
|
16: "\"We don't need this\" — until the FCC sends the letter",
|
|
17: "No USF. No CALEA. No state PUCs. No 499-A. Here's the setup.",
|
|
18: "Last email: save $55K+ in Year 1. Start at ~$975/mo \u2014 4 easy payments.",
|
|
22: "For counsel: $58K-$525K in avoided compliance costs for your carrier clients",
|
|
23: "Referral arrangement: $300 per carrier setup, zero liability for your firm",
|
|
}
|
|
|
|
CAMPAIGNS = [9, 10, 11, 12, 15, 16, 17, 18, 22, 23]
|
|
|
|
for cid in CAMPAIGNS:
|
|
d = api_get(f"/api/campaigns/{cid}")
|
|
data = d["data"]
|
|
body = data["body"]
|
|
name = data["name"]
|
|
|
|
if "What you're paying now" in body:
|
|
print(f" SKIP {cid:3d} | {name[:55]} | already has savings table")
|
|
continue
|
|
|
|
# Insert the savings table after the CTA button
|
|
# Find the CTA button (the red button link) and insert after it
|
|
cta_markers = [
|
|
'border-radius:4px;text-decoration:none', # CTA button style
|
|
'background:#e63f2a', # CTA button background
|
|
]
|
|
|
|
inserted = False
|
|
for marker in cta_markers:
|
|
if marker in body:
|
|
# Find the end of the CTA button row (</tr> after the button)
|
|
idx = body.index(marker)
|
|
# Find the next </tr></table></td></tr> after the button
|
|
close_idx = body.find("</td></tr>", idx)
|
|
if close_idx > 0:
|
|
close_idx += len("</td></tr>")
|
|
body = body[:close_idx] + SAVINGS_TABLE + body[close_idx:]
|
|
inserted = True
|
|
break
|
|
|
|
if not inserted:
|
|
# Fallback: insert before the chat block or footer
|
|
for fallback in ["We're online", "We are online", "style=\"display:block;margin:0 auto 10px;width:70px"]:
|
|
if fallback in body:
|
|
fb_idx = body.index(fallback)
|
|
tr_start = body[:fb_idx].rfind("<tr><td")
|
|
if tr_start > 0:
|
|
body = body[:tr_start] + SAVINGS_TABLE + body[tr_start:]
|
|
inserted = True
|
|
break
|
|
|
|
if not inserted:
|
|
print(f" FAIL {cid:3d} | {name[:55]} | no insertion point found")
|
|
continue
|
|
|
|
lists = [l["id"] for l in data.get("lists", [])]
|
|
new_subject = SUBJECT_UPDATES.get(cid, data["subject"])
|
|
old_subject = data["subject"]
|
|
result = api_put(f"/api/campaigns/{cid}", {
|
|
"name": data["name"],
|
|
"subject": new_subject,
|
|
"body": body,
|
|
"lists": lists,
|
|
"content_type": data.get("content_type", "richtext"),
|
|
"type": data.get("type", "regular"),
|
|
})
|
|
if "data" in result:
|
|
subj_changed = " | subject updated" if new_subject != old_subject else ""
|
|
print(f" OK {cid:3d} | {name[:55]} | +savings table{subj_changed}")
|
|
if new_subject != old_subject:
|
|
print(f" Old: {old_subject}")
|
|
print(f" New: {new_subject}")
|
|
else:
|
|
print(f" FAIL {cid:3d} | {name[:55]} | API error")
|
|
|
|
print("\nDone")
|