"""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()