From 2611b5458bfbd701399a774eecfb803d9068062c Mon Sep 17 00:00:00 2001 From: justin Date: Wed, 17 Jun 2026 23:40:01 -0500 Subject: [PATCH] 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. --- scripts/workers/campaign_helpers.py | 196 ++++++++++++++++++++ scripts/workers/create_crtc_usf_campaign.py | 156 ++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 scripts/workers/campaign_helpers.py create mode 100644 scripts/workers/create_crtc_usf_campaign.py diff --git a/scripts/workers/campaign_helpers.py b/scripts/workers/campaign_helpers.py new file mode 100644 index 0000000..026d28b --- /dev/null +++ b/scripts/workers/campaign_helpers.py @@ -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 {EMAIL} ' + f'or call {PHONE}' +) + + +# ── HTML helpers ───────────────────────────────────────────────────────────── + +def hdr(eyebrow, headline, sub=None): + s = f'

{sub}

' if sub else '' + return ( + '
' + '
' + '' + '' + '' + '
Performance WestTelecom Services
' + '
 
' + '
' + f'

{eyebrow}

' + f'

{headline}

{s}' + '
' + ) + + +def flagbar(left, right): + return ( + '
' + '' + '' + '' + '' + '
USA' + f'{left}Canada' + f'{right}
' + ) + + +def stats(*items): + w = 100 // len(items) + cells = "".join( + f'' + f'
' + f'
{v}
' + f'
{l}
' + '
' + for v, l in items + ) + return f'{cells}
' + + +def cta(text, url): + return ( + '
' + '' + f'' + '
{text}
' + ) + + +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'' + f'
' + f'
{n}
' + f'
{t}
' + '
' + for n, t in data[i:i+4] + ) + rows += f"{cells}" + return ( + '' + '
' + '

' + 'Join other US voice carriers registered to do business in Canada ' + 'Canada

' + f'{rows}
' + '
' + ) + + +def ftr(note=""): + note_html = f'{note}
' if note else '' + return ( + '' + '
' + 'Performance West' + '

' + 'Performance West Inc.  ·  performancewest.net

' + f'

{note_html}' + 'Unsubscribe

' + '
' + ) + + +def bq(t): + return ( + '' + '
{t}
' + ) + + +def P(t): + return f'

{t}

' + + +def PS(t): + return f'

{t}

' + + +def H2(t): + return f'

{t}

' + + +def UL(*items): + return ( + '' + ) + + +def assemble(hdr_html, fb_html, body_html, ftr_html): + inner = ( + hdr_html + fb_html + + f'
{body_html}
' + + ftr_html + ) + return ( + '' + '
' + '' + f'
{inner}
' + ) + + +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 diff --git a/scripts/workers/create_crtc_usf_campaign.py b/scripts/workers/create_crtc_usf_campaign.py new file mode 100644 index 0000000..5eb99b5 --- /dev/null +++ b/scripts/workers/create_crtc_usf_campaign.py @@ -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 ( + '' + '
' + '

Limited-time offer

' + f'

$200 off your Canadian carrier setup

' + f'

Use code {CODE} at checkout  ·  expires Friday at 11:59pm ET

' + '
' + ) + + +def guide_block(): + """PDF lead-magnet download block (Listmonk can't attach files).""" + return ( + '' + '
' + '' + '' + '
📄' + '

Free guide: Canadian Wholesale Carrier & Vendor Reference

' + '

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.

' + f'Download the PDF →' + '
' + '
' + ) + + +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 Q3 2026 USF contribution factor at 38.8% — 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
contribution factor"), + ("+1.8 pts", "increase over
Q2 (37.0%)"), + ("Jul 1", "effective date
(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( + "USF contributions — now 38.8% of interstate/international end-user revenue, filed and remitted via the 499", + "FCC Form 499-A / 499-Q — annual and quarterly revenue filings, with true-ups and audit exposure", + "Robocall Mitigation Database — annual recertification; miss it and your traffic gets blocked", + "STIR/SHAKEN — call-authentication implementation and ongoing attestation", + "CALEA — lawful-intercept capability, SSI filing, and the cost of a compliant solution", + "Section 214 + Team Telecom — for international service, with national-security review that can stall financings and M&A", + "State PUC registrations and FCC regulatory fees 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( + "No USF. Canada funds its contribution program differently — there is no 38.8% factor on your Canadian carrier’s revenue", + "No Robocall Mitigation Database recert and no FCC 499 for the Canadian entity", + "No CALEA mandate in the US sense — lawful-intercept obligations are far lighter and cheaper", + "No Section 214 / Team Telecom — CRTC registration is a notification, not an application with a national-security review", + "Same +1 country code. Your customers dial exactly the same way — nothing changes on their end", + "A clean second jurisdiction — 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 British Columbia or Ontario — a separate legal entity from your US company", + "CRTC registration (domestic reseller + BITS international authorization)", + "Canadian DID provisioned under your new carrier identity", + "Virtual registered office, .ca domain + 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%.
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())