"""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 = """
| Split it into 4 payments: Pay ~$975/month with Klarna Pay in 4. Start your Canadian carrier setup today — pay over time. |
|
"""
# 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 | containing a table with
background:#fef3c7 (yellow) and mentions of "off your setup" / coupon codes.
"""
# Pattern: match the entire |
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']*>\s*]*background:#fef3c7[^>]*>.*? \s* |
',
# Broader fallback: any row containing CRTC10 or CRTCFIVE code blocks
r'| ]*>.*?(?:CRTC10|CRTCFIVE).*? |
',
]
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()