CRTC USF campaign: shared campaign_helpers + Q3 38.8% USF email builder
- campaign_helpers.py: extract the branded Listmonk HTML helpers (hdr/flagbar/ stats/cta/footer/P/UL/etc.) + create_campaign() from create_campaigns.py into a side-effect-free shared module; create_campaign() now takes an altbody so every campaign ships a plaintext alternative (deliverability). - create_crtc_usf_campaign.py: build the one-off CRTC email hooked on the Q3 2026 USF factor (38.8%, +1.8pts, eff Jul 1), with a $200-off CANADA200 banner (expires Fri 23:59 ET, CTA links carry ?code= for auto-apply), the full US carrier burden vs Canada advantage, BC/ON incorporation, and a hosted carrier-guide PDF download. Creates a DRAFT only; sending stays manual.
This commit is contained in:
parent
e379e2b10f
commit
2611b5458b
2 changed files with 352 additions and 0 deletions
196
scripts/workers/campaign_helpers.py
Normal file
196
scripts/workers/campaign_helpers.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
campaign_helpers.py — Shared Listmonk campaign building blocks.
|
||||
|
||||
Branded, email-client-safe HTML helpers (header, flag bar, stat tiles, CTA
|
||||
button, footer, paragraphs, lists) plus the Listmonk POST helper. Extracted
|
||||
from create_campaigns.py so multiple campaign scripts (the original 7-email
|
||||
drips and one-off promos like the CRTC USF campaign) can share ONE source of
|
||||
truth for the look-and-feel and the API plumbing.
|
||||
|
||||
Importing this module has NO side effects (it does not POST anything).
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
# ── Listmonk connection ──────────────────────────────────────────────────────
|
||||
LISTMONK_URL = "https://lists.performancewest.net"
|
||||
AUTH = ("api", "6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y")
|
||||
|
||||
# ── Brand constants ──────────────────────────────────────────────────────────
|
||||
URL = "https://performancewest.net/order/canada-crtc"
|
||||
PHONE = "1-888-411-0383"
|
||||
EMAIL = "info@performancewest.net"
|
||||
CONTACT = (
|
||||
f'Email <a href="mailto:{EMAIL}" style="color:#e63f2a;text-decoration:none;">{EMAIL}</a> '
|
||||
f'or call <a href="tel:+18884110383" style="color:#e63f2a;text-decoration:none;">{PHONE}</a>'
|
||||
)
|
||||
|
||||
|
||||
# ── HTML helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def hdr(eyebrow, headline, sub=None):
|
||||
s = f'<p style="margin:8px 0 0;font-family:Arial,sans-serif;font-size:13px;color:#a0b4cc;line-height:1.5;">{sub}</p>' if sub else ''
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#1a2744;padding:0;">'
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding:18px 40px 14px;">'
|
||||
'<table cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||
'<td style="vertical-align:middle;padding-right:12px;"><img src="https://performancewest.net/images/logo.png" width="90" alt="Performance West" style="display:block;width:90px;height:auto;"></td>'
|
||||
'<td style="vertical-align:middle;border-left:1px solid #2d4e78;padding-left:12px;"><span style="color:#8fa8d0;font-family:Arial,sans-serif;font-size:11px;letter-spacing:1.5px;text-transform:uppercase;">Telecom Services</span></td>'
|
||||
'</tr></table></td></tr></table>'
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e63f2a;height:3px;font-size:0;line-height:0;"> </td></tr></table>'
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding:28px 40px 32px;">'
|
||||
f'<p style="margin:0 0 8px;font-family:Arial,sans-serif;font-size:11px;color:#e63f2a;letter-spacing:2px;text-transform:uppercase;font-weight:600;">{eyebrow}</p>'
|
||||
f'<h1 style="margin:0;font-family:Arial,sans-serif;font-size:24px;font-weight:700;color:#ffffff;line-height:1.3;">{headline}</h1>{s}'
|
||||
'</td></tr></table></td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def flagbar(left, right):
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#f0f4ff;padding:10px 40px;border-bottom:1px solid #dde5f0;">'
|
||||
'<table cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||
'<td style="padding-right:16px;vertical-align:middle;"><img src="https://performancewest.net/images/flags/usa.png" width="22" height="14" alt="USA" style="display:inline;vertical-align:middle;">'
|
||||
f'<span style="font-family:Arial,sans-serif;font-size:12px;color:#4a5568;margin-left:6px;vertical-align:middle;">{left}</span></td>'
|
||||
'<td style="padding:0 16px;color:#cbd5e0;font-size:14px;vertical-align:middle;">→</td>'
|
||||
'<td style="vertical-align:middle;"><img src="https://performancewest.net/images/flags/canada.png" width="22" height="14" alt="Canada" style="display:inline;vertical-align:middle;">'
|
||||
f'<span style="font-family:Arial,sans-serif;font-size:12px;color:#4a5568;margin-left:6px;vertical-align:middle;">{right}</span></td>'
|
||||
'</tr></table></td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def stats(*items):
|
||||
w = 100 // len(items)
|
||||
cells = "".join(
|
||||
f'<td width="{w}%" style="padding:0 6px;"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||
f'<td style="background:#1a2744;border-radius:6px;padding:18px 12px;text-align:center;">'
|
||||
f'<div style="font-family:Arial,sans-serif;font-size:26px;font-weight:700;color:#ffffff;line-height:1;">{v}</div>'
|
||||
f'<div style="font-family:Arial,sans-serif;font-size:11px;color:#8fa8d0;margin-top:6px;line-height:1.4;">{l}</div>'
|
||||
'</td></tr></table></td>'
|
||||
for v, l in items
|
||||
)
|
||||
return f'<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;"><tr>{cells}</tr></table>'
|
||||
|
||||
|
||||
def cta(text, url):
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center">'
|
||||
'<table cellpadding="0" cellspacing="0" border="0" style="margin:28px auto;"><tr>'
|
||||
f'<td style="background:#e63f2a;border-radius:4px;"><a href="{url}" style="display:inline-block;padding:14px 36px;font-family:Arial,sans-serif;font-size:15px;font-weight:700;color:#ffffff;text-decoration:none;">{text}</a></td>'
|
||||
'</tr></table></td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def carriers_block():
|
||||
data = [
|
||||
("Twilio","NASDAQ: TWLO"),("Bandwidth","NASDAQ: BAND"),("Telnyx","Washington DC"),("RingCentral","NYSE: RNG"),
|
||||
("Vonage","Holmdel NJ"),("8x8","NASDAQ: EGHT"),("Zoom Phone","NASDAQ: ZM"),("Dialpad","San Ramon CA"),
|
||||
("Google Voice","Alphabet / GOOGL"),("Ooma","NYSE: OOMA"),("Onvoy / Sinch","OMX: SINCH"),("Sangoma","NASDAQ: SANG"),
|
||||
]
|
||||
rows = ""
|
||||
for i in range(0, len(data), 4):
|
||||
cells = "".join(
|
||||
f'<td width="25%" style="padding:4px;"><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||
f'<td style="background:rgba(255,255,255,0.09);border:1px solid rgba(255,255,255,0.15);border-radius:6px;padding:8px 6px;text-align:center;">'
|
||||
f'<div style="font-family:Arial,sans-serif;font-size:12px;font-weight:600;color:#ffffff;">{n}</div>'
|
||||
f'<div style="font-family:Arial,sans-serif;font-size:10px;color:#fca5a5;margin-top:2px;">{t}</div>'
|
||||
'</td></tr></table></td>'
|
||||
for n, t in data[i:i+4]
|
||||
)
|
||||
rows += f"<tr>{cells}</tr>"
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:28px 0;"><tr>'
|
||||
'<td style="background:#5c0a0a;border-radius:8px;padding:20px 24px;">'
|
||||
'<p style="margin:0 0 14px;font-family:Arial,sans-serif;font-size:14px;font-weight:700;color:#ffffff;text-align:center;">'
|
||||
'Join other US voice carriers registered to do business in Canada '
|
||||
'<img src="https://performancewest.net/images/flags/canada.png" width="22" height="14" alt="Canada" style="display:inline;vertical-align:middle;margin-left:6px;"></p>'
|
||||
f'<table width="100%" cellpadding="0" cellspacing="0" border="0">{rows}</table>'
|
||||
'</td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def ftr(note=""):
|
||||
note_html = f'{note}<br>' if note else ''
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||
'<td style="background:#f4f5f7;padding:20px 40px;border-top:1px solid #e8ecf0;text-align:center;">'
|
||||
'<img src="https://performancewest.net/images/logo.png" width="70" alt="Performance West" style="display:block;margin:0 auto 10px;width:70px;height:auto;opacity:0.5;">'
|
||||
'<p style="margin:0 0 6px;font-family:Arial,sans-serif;font-size:12px;color:#9ca3af;">'
|
||||
'Performance West Inc. · <a href="https://performancewest.net" style="color:#9ca3af;">performancewest.net</a></p>'
|
||||
f'<p style="margin:0;font-family:Arial,sans-serif;font-size:11px;color:#b0b7c3;line-height:1.6;">{note_html}'
|
||||
'<a href="{{ UnsubscribeURL }}" style="color:#b0b7c3;">Unsubscribe</a></p>'
|
||||
'</td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def bq(t):
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:20px 0;"><tr>'
|
||||
'<td style="border-left:4px solid #e63f2a;padding:12px 20px;background:#fdf4f3;'
|
||||
f'font-family:Arial,sans-serif;font-size:15px;color:#2d3748;line-height:1.7;font-style:italic;">{t}</td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def P(t):
|
||||
return f'<p style="margin:0 0 16px;font-family:Arial,sans-serif;font-size:15px;color:#2d3748;line-height:1.7;">{t}</p>'
|
||||
|
||||
|
||||
def PS(t):
|
||||
return f'<p style="margin:0 0 16px;font-family:Arial,sans-serif;font-size:14px;color:#6b7280;line-height:1.7;">{t}</p>'
|
||||
|
||||
|
||||
def H2(t):
|
||||
return f'<h2 style="margin:24px 0 10px;font-family:Arial,sans-serif;font-size:17px;font-weight:700;color:#1a2744;">{t}</h2>'
|
||||
|
||||
|
||||
def UL(*items):
|
||||
return (
|
||||
'<ul style="margin:0 0 16px;padding-left:22px;font-family:Arial,sans-serif;font-size:15px;color:#2d3748;line-height:1.7;">'
|
||||
+ "".join(f'<li style="margin-bottom:8px;">{i}</li>' for i in items)
|
||||
+ '</ul>'
|
||||
)
|
||||
|
||||
|
||||
def assemble(hdr_html, fb_html, body_html, ftr_html):
|
||||
inner = (
|
||||
hdr_html + fb_html
|
||||
+ f'<table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding:32px 40px;background:#ffffff;" class="body-pad">{body_html}</td></tr></table>'
|
||||
+ ftr_html
|
||||
)
|
||||
return (
|
||||
'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>'
|
||||
'@media only screen and (max-width:600px){.wrap{width:100%!important;border-radius:0!important;}.body-pad{padding:24px 20px!important;}h1{font-size:20px!important;}}'
|
||||
'body,table,td,p,a{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}table{border-collapse:collapse!important;}img{border:0;outline:none;text-decoration:none;}'
|
||||
'</style></head><body style="margin:0;padding:0;background:#eef0f3;">'
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#eef0f3;padding:20px 0;"><tr><td align="center">'
|
||||
'<table width="620" cellpadding="0" cellspacing="0" border="0" class="wrap" style="width:620px;max-width:620px;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">'
|
||||
f'<tr><td>{inner}</td></tr></table></td></tr></table></body></html>'
|
||||
)
|
||||
|
||||
|
||||
def create_campaign(name, subject, lists, body_html, altbody=None, status="draft"):
|
||||
"""POST a campaign to Listmonk.
|
||||
|
||||
altbody: optional plaintext alternative. Strongly recommended for
|
||||
deliverability — an HTML-only campaign is a spam signal. Pass the output of
|
||||
scripts._email_plaintext.html_to_text(body_html) (or a hand-written
|
||||
plaintext). When omitted, Listmonk generates its own plaintext at send time.
|
||||
"""
|
||||
s = requests.Session()
|
||||
s.auth = AUTH
|
||||
payload = {
|
||||
"name": name, "subject": subject, "lists": lists,
|
||||
"type": "regular", "content_type": "html",
|
||||
"body": body_html, "status": status,
|
||||
}
|
||||
if altbody is not None:
|
||||
payload["altbody"] = altbody
|
||||
r = s.post(f"{LISTMONK_URL}/api/campaigns", json=payload, timeout=30)
|
||||
if not r.ok:
|
||||
print(f" ERROR {r.status_code}: {r.text[:200]}", file=sys.stderr)
|
||||
return None
|
||||
cid = r.json().get('data', {}).get('id', '?')
|
||||
print(f" [{cid}] {name}")
|
||||
return cid
|
||||
156
scripts/workers/create_crtc_usf_campaign.py
Normal file
156
scripts/workers/create_crtc_usf_campaign.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
create_crtc_usf_campaign.py — One-off CRTC marketing email hooked on the
|
||||
Q3 2026 USF contribution-factor increase.
|
||||
|
||||
Audience : Listmonk list 3 (FCC Carriers - Direct Contacts, ~6,046 enabled).
|
||||
Hook : Q3 2026 federal USF contribution factor = 38.8% (up from 37.0% in
|
||||
Q2), effective Jul 1 (FCC PN DA-26-546A1).
|
||||
Offer : $200 off the CRTC carrier package service fee with code CANADA200,
|
||||
valid through Fri Jun 19 2026 23:59 ET. The CRTC order page
|
||||
auto-applies ?code= from the URL, so the CTA links carry it.
|
||||
Lead : "Canadian Wholesale Carrier & Vendor Reference Guide" PDF, hosted at
|
||||
magnet performancewest.net/guides/canada-carrier-guide.pdf (Listmonk
|
||||
campaigns can't attach files, so we link a prominent download).
|
||||
|
||||
Creates the campaign in Listmonk as a DRAFT. Sending is a separate, manual,
|
||||
STOP-and-confirm step in the Listmonk UI (or set status via API). Run:
|
||||
|
||||
python3 scripts/workers/create_crtc_usf_campaign.py # draft on list 3
|
||||
python3 scripts/workers/create_crtc_usf_campaign.py --test # draft to a test list/email
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Make `scripts` importable whether run from repo root or scripts/workers.
|
||||
_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if _REPO not in sys.path:
|
||||
sys.path.insert(0, _REPO)
|
||||
|
||||
from scripts._email_plaintext import html_to_text # noqa: E402
|
||||
from scripts.workers.campaign_helpers import ( # noqa: E402
|
||||
CONTACT, P, PS, H2, UL, bq, cta, hdr, flagbar, stats, assemble, ftr,
|
||||
create_campaign,
|
||||
)
|
||||
|
||||
LIST_ID = 3
|
||||
CODE = "CANADA200"
|
||||
GUIDE_URL = "https://performancewest.net/guides/canada-carrier-guide.pdf"
|
||||
ORDER_URL = f"https://performancewest.net/order/canada-crtc?code={CODE}"
|
||||
|
||||
|
||||
def coupon_banner():
|
||||
"""Prominent $200-off banner with expiry."""
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:8px 0 24px;"><tr>'
|
||||
'<td style="background:#0f7a3d;border-radius:8px;padding:18px 24px;text-align:center;">'
|
||||
'<p style="margin:0 0 4px;font-family:Arial,sans-serif;font-size:13px;color:#bdf0cf;letter-spacing:1px;text-transform:uppercase;font-weight:600;">Limited-time offer</p>'
|
||||
f'<p style="margin:0 0 6px;font-family:Arial,sans-serif;font-size:22px;font-weight:800;color:#ffffff;line-height:1.2;">$200 off your Canadian carrier setup</p>'
|
||||
f'<p style="margin:0;font-family:Arial,sans-serif;font-size:14px;color:#d6f5e1;">Use code <span style="font-family:\'Courier New\',monospace;font-weight:700;color:#ffffff;background:rgba(255,255,255,0.18);padding:2px 8px;border-radius:4px;">{CODE}</span> at checkout · expires <strong style="color:#ffffff;">Friday at 11:59pm ET</strong></p>'
|
||||
'</td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def guide_block():
|
||||
"""PDF lead-magnet download block (Listmonk can't attach files)."""
|
||||
return (
|
||||
'<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:24px 0;"><tr>'
|
||||
'<td style="background:#f0f4ff;border:1px solid #dde5f0;border-radius:8px;padding:20px 24px;">'
|
||||
'<table cellpadding="0" cellspacing="0" border="0"><tr>'
|
||||
'<td style="vertical-align:middle;padding-right:16px;font-size:34px;line-height:1;">📄</td>'
|
||||
'<td style="vertical-align:middle;">'
|
||||
'<p style="margin:0 0 4px;font-family:Arial,sans-serif;font-size:15px;font-weight:700;color:#1a2744;">Free guide: Canadian Wholesale Carrier & Vendor Reference</p>'
|
||||
'<p style="margin:0 0 10px;font-family:Arial,sans-serif;font-size:13px;color:#4a5568;line-height:1.6;">12 vetted Canadian wholesale partners for voice termination, DIDs, SIP trunking and UCaaS — the upstream vendors you’ll work with once your Canadian carrier is live.</p>'
|
||||
f'<a href="{GUIDE_URL}" style="display:inline-block;font-family:Arial,sans-serif;font-size:14px;font-weight:700;color:#1e40af;text-decoration:none;">Download the PDF →</a>'
|
||||
'</td></tr></table>'
|
||||
'</td></tr></table>'
|
||||
)
|
||||
|
||||
|
||||
def build_body():
|
||||
return (
|
||||
P("Hi {{ .Subscriber.FirstName }},")
|
||||
+ P("If you contribute to the federal Universal Service Fund, your Q3 number just went up again.")
|
||||
+ bq("The FCC has set the <strong>Q3 2026 USF contribution factor at 38.8%</strong> — up from 37.0% in Q2, and effective July 1. That is the rate you remit on the interstate and international end-user revenue you report on your 499.")
|
||||
+ P("38.8% is near the highest the factor has ever been. A decade ago it sat in the mid-teens. For a small or mid-size US carrier, that is a steadily rising tax on every interstate dollar you bill — on top of everything else the FCC requires.")
|
||||
+ stats(
|
||||
("38.8%", "Q3 2026 USF<br>contribution factor"),
|
||||
("+1.8 pts", "increase over<br>Q2 (37.0%)"),
|
||||
("Jul 1", "effective date<br>(FCC DA-26-546A1)"),
|
||||
)
|
||||
+ H2("The US carrier burden, in one place.")
|
||||
+ P("USF is just the line item that moved this quarter. The full load a registered US carrier carries:")
|
||||
+ UL(
|
||||
"<strong>USF contributions</strong> — now 38.8% of interstate/international end-user revenue, filed and remitted via the 499",
|
||||
"<strong>FCC Form 499-A / 499-Q</strong> — annual and quarterly revenue filings, with true-ups and audit exposure",
|
||||
"<strong>Robocall Mitigation Database</strong> — annual recertification; miss it and your traffic gets blocked",
|
||||
"<strong>STIR/SHAKEN</strong> — call-authentication implementation and ongoing attestation",
|
||||
"<strong>CALEA</strong> — lawful-intercept capability, SSI filing, and the cost of a compliant solution",
|
||||
"<strong>Section 214 + Team Telecom</strong> — for international service, with national-security review that can stall financings and M&A",
|
||||
"<strong>State PUC registrations</strong> and <strong>FCC regulatory fees</strong> on top of the federal load",
|
||||
)
|
||||
+ H2("Why smaller carriers are standing up a Canadian operation.")
|
||||
+ P("A CRTC-registered Canadian carrier is a separate legal entity in a separate regulatory jurisdiction. For the voice traffic you move there, the US compliance stack simply does not apply:")
|
||||
+ UL(
|
||||
"<strong>No USF.</strong> Canada funds its contribution program differently — there is no 38.8% factor on your Canadian carrier’s revenue",
|
||||
"<strong>No Robocall Mitigation Database recert</strong> and <strong>no FCC 499</strong> for the Canadian entity",
|
||||
"<strong>No CALEA mandate</strong> in the US sense — lawful-intercept obligations are far lighter and cheaper",
|
||||
"<strong>No Section 214 / Team Telecom</strong> — CRTC registration is a notification, not an application with a national-security review",
|
||||
"<strong>Same +1 country code.</strong> Your customers dial exactly the same way — nothing changes on their end",
|
||||
"<strong>A clean second jurisdiction</strong> — an FCC enforcement action against your US entity does not reach a Canadian corporation",
|
||||
)
|
||||
+ bq("You do not give up your US business. You add a Canadian carrier alongside it — for the voice traffic that doesn’t need to sit under the FCC, and for the Canadian market you can now sell into.")
|
||||
+ H2("What we set up — turnkey, in 6–10 weeks.")
|
||||
+ UL(
|
||||
"Incorporation in <strong>British Columbia or Ontario</strong> — a separate legal entity from your US company",
|
||||
"<strong>CRTC registration</strong> (domestic reseller + BITS international authorization)",
|
||||
"Canadian DID provisioned under your new carrier identity",
|
||||
"Virtual registered office, <strong>.ca domain</strong> + up to 14 email addresses",
|
||||
"Full corporate binder, delivered digitally — plus a Canadian business-banking referral",
|
||||
)
|
||||
+ coupon_banner()
|
||||
+ cta(f"Start your Canadian carrier setup — $200 off →", ORDER_URL)
|
||||
+ guide_block()
|
||||
+ PS(f"Questions about how the Canadian structure would work for your traffic? {CONTACT}. The {CODE} discount is good through Friday at 11:59pm ET.")
|
||||
+ P("— Performance West")
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--test", action="store_true", help="create against a test list id (env CRTC_TEST_LIST) instead of list 3")
|
||||
ap.add_argument("--name", default="CRTC USF Q3 \u2014 38.8% increase + $200 off (CANADA200)")
|
||||
ap.add_argument("--subject", default="USF jumps to 38.8% in Q3 \u2014 here\u2019s the Canadian alternative ($200 off)")
|
||||
args = ap.parse_args()
|
||||
|
||||
lists = [int(os.getenv("CRTC_TEST_LIST", "0"))] if args.test else [LIST_ID]
|
||||
if args.test and lists == [0]:
|
||||
print("--test requires CRTC_TEST_LIST env var (a test list id)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
body = assemble(
|
||||
hdr(
|
||||
"USF Increase \u2014 Q3 2026",
|
||||
"USF just hit 38.8%.<br>There’s a Canadian alternative.",
|
||||
"The federal contribution factor rose again, effective July 1",
|
||||
),
|
||||
flagbar(
|
||||
"US carrier \u2014 38.8% USF + the full FCC stack",
|
||||
"Canadian CRTC carrier \u2014 no USF, separate jurisdiction",
|
||||
),
|
||||
build_body(),
|
||||
ftr(""),
|
||||
)
|
||||
altbody = html_to_text(body)
|
||||
|
||||
print(f"=== Creating CRTC USF campaign (list {lists}) ===")
|
||||
cid = create_campaign(args.name, args.subject, lists, body, altbody=altbody, status="draft")
|
||||
if cid:
|
||||
print(f"\nDraft created (id {cid}). Review/preview in Listmonk, then send manually.")
|
||||
print(f"Body: {len(body):,} chars HTML / {len(altbody):,} chars plaintext")
|
||||
return 0 if cid else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue