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>
186 lines
7 KiB
Python
186 lines
7 KiB
Python
"""Replace 5% off / discount coupon pitch with 4-payments plan in all remaining Listmonk campaigns.
|
|
|
|
Targets draft and scheduled campaigns only. Updates:
|
|
- Subject lines mentioning "5% off", "CRTCFIVE", "CRTC10"
|
|
- Discount blocks in the email body (yellow box with coupon code)
|
|
- CTA URLs containing ?code=CRTCFIVE or ?code=CRTC10
|
|
"""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
LISTMONK = "http://localhost:9100"
|
|
API_USER = "api"
|
|
API_PASS = "6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y"
|
|
|
|
ORDER_PAGE = "https://performancewest.net/order/canada-crtc"
|
|
|
|
# New 4-payments block (replaces the discount/coupon block)
|
|
PAY4_BLOCK = """<tr><td style="padding:20px 40px 10px 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>"""
|
|
|
|
# Subject line replacements for campaigns that currently mention 5% off
|
|
SUBJECT_REPLACEMENTS = {
|
|
12: "Last email: $55K\u2013$525K in savings \u2014 start at ~$975 with 4 payments",
|
|
18: "Last email: save $55K+ in Year 1. Start at ~$975/mo \u2014 4 easy payments.",
|
|
}
|
|
|
|
|
|
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 strip_coupon_from_url(body):
|
|
"""Remove ?code=CRTCFIVE, ?code=CRTC10, &code=... from URLs."""
|
|
body = re.sub(r'[?&]code=CRTC(?:FIVE|10)\b', '', body)
|
|
return body
|
|
|
|
|
|
def replace_discount_block(body):
|
|
"""Replace the yellow discount block with the 4-payments block.
|
|
|
|
The discount block is a <tr><td> containing a table with
|
|
background:#fef3c7 (yellow) and mentions of "off your setup" / coupon codes.
|
|
"""
|
|
# Pattern: match the entire <tr><td...> row that contains the discount box
|
|
# The block uses background:#fef3c7 and mentions coupon/discount codes
|
|
patterns = [
|
|
# Match the full discount row (from fix_chat_blocks.py / update_campaign_ctas.py)
|
|
r'<tr><td[^>]*>\s*<table[^>]*background:#fef3c7[^>]*>.*?</table>\s*</td></tr>',
|
|
# Broader fallback: any row containing CRTC10 or CRTCFIVE code blocks
|
|
r'<tr><td[^>]*>.*?(?:CRTC10|CRTCFIVE).*?</td></tr>',
|
|
]
|
|
replaced = False
|
|
for pat in patterns:
|
|
new_body, n = re.subn(pat, PAY4_BLOCK, body, count=1, flags=re.DOTALL)
|
|
if n > 0:
|
|
body = new_body
|
|
replaced = True
|
|
break
|
|
return body, replaced
|
|
|
|
|
|
def replace_subject(subject, campaign_id):
|
|
"""Replace 5%-off subject lines. Returns (new_subject, changed)."""
|
|
if campaign_id in SUBJECT_REPLACEMENTS:
|
|
new = SUBJECT_REPLACEMENTS[campaign_id]
|
|
if new != subject:
|
|
return new, True
|
|
# Generic fallback: replace "5% off" mentions in any other subject
|
|
if "5% off" in subject.lower() or "crtcfive" in subject.lower() or "crtc10" in subject.lower():
|
|
subject = subject.replace("5% off today", "start at ~$975/mo \u2014 4 payments")
|
|
subject = subject.replace("5% off", "4 easy payments")
|
|
subject = re.sub(r'Use code CRTCFIVE for ', '', subject, flags=re.IGNORECASE)
|
|
subject = re.sub(r'Use code CRTC10[^.]*\.?', '', subject, flags=re.IGNORECASE)
|
|
return subject.strip(), True
|
|
return subject, False
|
|
|
|
|
|
def main():
|
|
dry_run = "--dry-run" in sys.argv
|
|
|
|
if dry_run:
|
|
print("=== DRY RUN === (no changes will be made)\n")
|
|
|
|
# Fetch all campaigns
|
|
result = curl_get("/api/campaigns?per_page=100")
|
|
campaigns = result.get("data", {}).get("results", [])
|
|
if not campaigns:
|
|
print("No campaigns found.")
|
|
return
|
|
|
|
print(f"Found {len(campaigns)} total campaigns.\n")
|
|
|
|
updated = 0
|
|
skipped = 0
|
|
|
|
for c in campaigns:
|
|
cid = c["id"]
|
|
name = c["name"]
|
|
status = c.get("status", "?")
|
|
subject = c.get("subject", "")
|
|
|
|
if status not in ("draft", "scheduled"):
|
|
print(f" SKIP {cid:3d} | {name[:50]:50s} | status={status}")
|
|
skipped += 1
|
|
continue
|
|
|
|
# Fetch full campaign body
|
|
full = curl_get(f"/api/campaigns/{cid}")
|
|
data = full.get("data", {})
|
|
body = data.get("body", "")
|
|
|
|
changes = []
|
|
|
|
# 1. Replace subject line
|
|
new_subject, subj_changed = replace_subject(subject, cid)
|
|
if subj_changed:
|
|
changes.append(f"subject: \"{subject}\" -> \"{new_subject}\"")
|
|
|
|
# 2. Replace discount block in body
|
|
new_body, block_replaced = replace_discount_block(body)
|
|
if block_replaced:
|
|
changes.append("replaced discount block with 4-payments block")
|
|
|
|
# 3. Strip coupon codes from URLs
|
|
stripped_body = strip_coupon_from_url(new_body)
|
|
if stripped_body != new_body:
|
|
changes.append("removed coupon code from CTA URLs")
|
|
new_body = stripped_body
|
|
|
|
# 4. Replace any remaining text mentions of "5% off" in body
|
|
if "5% off" in new_body:
|
|
new_body = new_body.replace("5% off your setup:", "Split it into 4 payments:")
|
|
new_body = new_body.replace("5% off", "4 interest-free payments")
|
|
changes.append("replaced inline 5% off text references")
|
|
|
|
if not changes:
|
|
print(f" SKIP {cid:3d} | {name[:50]:50s} | no discount/coupon content found")
|
|
skipped += 1
|
|
continue
|
|
|
|
# Apply update
|
|
print(f" {'WOULD UPDATE' if dry_run else 'UPDATE'} {cid:3d} | {name[:50]}")
|
|
for ch in changes:
|
|
print(f" {ch}")
|
|
|
|
if not dry_run:
|
|
lists = [l["id"] for l in data.get("lists", [])]
|
|
update_data = {
|
|
"name": data["name"],
|
|
"subject": new_subject if subj_changed else subject,
|
|
"body": new_body,
|
|
"lists": lists,
|
|
"content_type": data.get("content_type", "richtext"),
|
|
"type": data.get("type", "regular"),
|
|
}
|
|
result = curl_put(f"/api/campaigns/{cid}", update_data)
|
|
if "data" in result:
|
|
print(f" -> OK")
|
|
updated += 1
|
|
else:
|
|
print(f" -> FAIL: {result}")
|
|
else:
|
|
updated += 1
|
|
|
|
print(f"\nDone: {updated} {'would be ' if dry_run else ''}updated, {skipped} skipped")
|
|
if dry_run:
|
|
print("\nRun without --dry-run to apply changes.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|