From 8485bba51d02f7e70ff2ed586d3083cc27cb4344 Mon Sep 17 00:00:00 2001 From: justin Date: Mon, 1 Jun 2026 20:00:46 -0500 Subject: [PATCH] Add trucking campaign setup script --- scripts/setup_trucking_campaigns.py | 293 ++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 scripts/setup_trucking_campaigns.py diff --git a/scripts/setup_trucking_campaigns.py b/scripts/setup_trucking_campaigns.py new file mode 100644 index 0000000..9f0cbc5 --- /dev/null +++ b/scripts/setup_trucking_campaigns.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +"""Create non-MCS-150 trucking Listmonk campaigns and send owner tests. + +Creates draft campaigns only. It does not schedule or start any bulk send. + +Usage: + python3 scripts/setup_trucking_campaigns.py + LISTMONK_URL=https://lists.performancewest.net CAMPAIGN_TEST_EMAIL=carrierone@gmx.com python3 scripts/setup_trucking_campaigns.py +""" +from __future__ import annotations + +import base64 +import json +import os +import urllib.error +import urllib.parse +import urllib.request + +LISTMONK_URL = os.getenv("LISTMONK_URL", "https://lists.performancewest.net").rstrip("/") +LISTMONK_USER = os.getenv("LISTMONK_USER", "api") +LISTMONK_PASS = os.getenv("LISTMONK_PASS", "6X1rKPea61N4rZ1S65Hx5zvqzbCj30F6nvEe9oVGH_Y") +TEST_EMAIL = os.getenv("CAMPAIGN_TEST_EMAIL", "carrierone@gmx.com") +FROM_EMAIL = "Performance West " +REPLY_TO = "info@performancewest.net" + +AUTH = base64.b64encode(f"{LISTMONK_USER}:{LISTMONK_PASS}".encode()).decode() + + +def api(path: str, data: dict | None = None, method: str | None = None) -> dict: + headers = { + "Authorization": f"Basic {AUTH}", + "Content-Type": "application/json", + "Accept": "application/json", + } + req = urllib.request.Request(f"{LISTMONK_URL}/api{path}", headers=headers) + if data is not None: + req.data = json.dumps(data).encode() + if method: + req.method = method + try: + raw = urllib.request.urlopen(req, timeout=45).read() + return json.loads(raw or b"{}") + except urllib.error.HTTPError as exc: + body = exc.read().decode(errors="replace")[:1000] + raise RuntimeError(f"Listmonk {path} HTTP {exc.code}: {body}") from exc + + +def list_map() -> dict[str, dict]: + resp = api("/lists?per_page=all") + results = resp.get("data", {}).get("results", []) + return {row["name"]: row for row in results} + + +def get_or_create_list(name: str, tags: list[str]) -> int: + lists = list_map() + if name in lists: + return int(lists[name]["id"]) + resp = api("/lists", {"name": name, "type": "private", "optin": "single", "tags": tags}, "POST") + return int(resp["data"]["id"]) + + +def service_rows(rows: list[tuple[str, str]]) -> str: + body = "".join( + f'{name}' + f'{price}' + for name, price in rows + ) + return f'{body}
' + + +def requirement_rows(rows: list[str]) -> str: + cells = "".join( + f'{row}' + for row in rows[:-1] + ) + cells += f'{rows[-1]}' + return f'{cells}
' + + +def build_email(cfg: dict) -> str: + cta_url = cfg["cta_url"] + cta = f"{cta_url}{'&' if '?' in cta_url else '?'}utm_source=listmonk&utm_medium=email&utm_campaign={cfg['slug']}" + return f''' +
+ + + +
''' + + +CAMPAIGNS = [ + { + "slug": "ca-mcp-carb", "list": "FMCSA State - California (MCP/CARB)", + "name": "CA Motor Carrier Permit + CARB Compliance Alert", "headline": "California Compliance Alert", + "subject": "California MCP + CARB requirements for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Motor Carrier Permit (MCP) — California requires many for-hire carriers and qualifying CMVs to maintain an MCP through CA DMV.", + "CA Number / CHP requirements — California carriers may need a state carrier number in addition to USDOT.", + "CARB emissions compliance — diesel vehicles can face California emissions restrictions and enforcement.", + ], + "services": [("California MCP + CARB Compliance", "$349"), ("IRP Registration Assistance", "$199"), ("IFTA Application + Decals", "$149"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/california/", "cta_text": "View California Compliance Services", + }, + { + "slug": "or-weight-mile", "list": "FMCSA State - Oregon (Weight-Mile Tax)", + "name": "Oregon Weight-Mile Tax Compliance Alert", "headline": "Oregon Weight-Mile Alert", + "subject": "Oregon Weight-Mile Tax registration for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Weight-Mile Tax — Oregon trucks over 26,000 lbs generally need weight-mile tax registration and filings.", + "IRP / apportioned plates — interstate Oregon operations often require apportioned registration.", + "ODOT authority — some for-hire intrastate operations require state authority.", + ], + "services": [("Oregon Weight-Mile Tax Setup", "$199"), ("IRP Registration Assistance", "$199"), ("IFTA Application + Decals", "$149"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/oregon/", "cta_text": "View Oregon Compliance Services", + }, + { + "slug": "ny-hut", "list": "FMCSA State - New York (HUT)", + "name": "NY Highway Use Tax Compliance Alert", "headline": "New York HUT Alert", + "subject": "New York HUT registration for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Highway Use Tax (HUT) — New York requires HUT registration for many vehicles over 18,000 lbs.", + "Quarterly filings — HUT is not just a one-time registration. Reporting may be recurring.", + "Intrastate authority — for-hire NY intrastate carriers may need NY authority.", + ], + "services": [("NY Highway Use Tax Registration", "$199"), ("HUT Quarterly Filing", "$99"), ("Intrastate Operating Authority", "$249"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/new-york/", "cta_text": "View New York Compliance Services", + }, + { + "slug": "ky-kyu", "list": "FMCSA State - Kentucky (KYU)", + "name": "Kentucky KYU Weight-Distance Tax Alert", "headline": "Kentucky KYU Alert", + "subject": "Kentucky KYU number requirements for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "KYU number — Kentucky requires weight-distance tax registration for vehicles over 59,999 lbs combined license weight.", + "Mileage reporting — KYU tax is based on miles operated in Kentucky and requires ongoing filings.", + "Permit exposure — operating without proper KYU setup can create roadside and tax issues.", + ], + "services": [("Kentucky KYU Number Setup", "$199"), ("KYU Quarterly Filing", "$99"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/kentucky/", "cta_text": "View Kentucky Compliance Services", + }, + { + "slug": "nm-weight-distance", "list": "FMCSA State - New Mexico (Weight-Distance)", + "name": "New Mexico Weight-Distance Tax Alert", "headline": "New Mexico Weight-Distance Alert", + "subject": "New Mexico Weight-Distance Tax for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Weight-Distance Tax — New Mexico requires qualifying commercial vehicles to register and file weight-distance returns.", + "Trip and mileage records — filings depend on New Mexico miles and vehicle weight class.", + "Operating state gap — carriers passing through NM can trigger requirements even if based elsewhere.", + ], + "services": [("New Mexico Weight-Distance Setup", "$199"), ("Quarterly Filing", "$99"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/new-mexico/", "cta_text": "View New Mexico Compliance Services", + }, + { + "slug": "ct-huf", "list": "FMCSA State - Connecticut (Highway Use Fee)", + "name": "Connecticut Highway Use Fee Alert", "headline": "Connecticut Highway Use Fee Alert", + "subject": "Connecticut Highway Use Fee for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Highway Use Fee — Connecticut applies a per-mile highway use fee to certain heavy vehicles over 26,000 lbs.", + "Quarterly filing — HUF returns are recurring and based on mileage and weight class.", + "Multi-state exposure — CT trips can trigger filing duties even for carriers based outside Connecticut.", + ], + "services": [("Connecticut HUF Registration", "$199"), ("Quarterly Filing", "$99"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/connecticut/", "cta_text": "View Connecticut Compliance Services", + }, + { + "slug": "tx-txdmv", "list": "FMCSA State - Texas (TxDMV)", + "name": "Texas TxDMV Number / Intrastate Authority Alert", "headline": "Texas TxDMV Alert", + "subject": "Texas TxDMV number requirements for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "TxDMV number — many Texas intrastate motor carriers need a TxDMV number in addition to USDOT.", + "Intrastate operating authority — for-hire Texas operations can trigger state authority and insurance filing requirements.", + "Insurance filing — Texas authority often requires proof of insurance on file with the state.", + ], + "services": [("Texas TxDMV Number Filing", "$249"), ("Texas Intrastate Authority", "$249"), ("State Insurance Filing", "$99"), ("State Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/texas/", "cta_text": "View Texas Compliance Services", + }, + { + "slug": "irp-ifta-starter", "list": "FMCSA Trucking - IRP IFTA Prospects", + "name": "IRP / IFTA Interstate Carrier Starter Campaign", "headline": "IRP / IFTA Filing Alert", + "subject": "Crossing state lines? IRP and IFTA checklist for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "IRP apportioned registration — qualifying interstate vehicles generally need apportioned plates through their base state.", + "IFTA fuel tax account — interstate fuel tax registration and decals may be required before operating across state lines.", + "Quarterly IFTA returns — IFTA continues after registration with quarterly fuel tax reporting.", + ], + "services": [("IRP Registration Assistance", "$199"), ("IFTA Application + Decals", "$149"), ("Quarterly IFTA Filing", "$99"), ("IRP/IFTA Bundle", "$299")], + "cta_url": "https://performancewest.net/services/trucking/irp-ifta/", "cta_text": "Get IRP / IFTA Help", + }, + { + "slug": "dot-drug-alcohol", "list": "FMCSA Trucking - Drug Alcohol Prospects", + "name": "DOT Drug & Alcohol Program Campaign", "headline": "DOT Drug & Alcohol Alert", + "subject": "DOT drug and alcohol testing requirements for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "CDL driver testing — carriers with CDL drivers generally need a DOT-compliant drug and alcohol testing program.", + "Random consortium — owner-operators usually need to be enrolled in a compliant random testing pool.", + "Audit exposure — missing testing records are common issues during safety audits and investigations.", + ], + "services": [("DOT Drug & Alcohol Program", "$149/yr"), ("Clearinghouse / Driver File Review", "$99"), ("DOT Full Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/drug-alcohol-program/", "cta_text": "Enroll in DOT D&A Program", + }, + { + "slug": "hazmat-compliance", "list": "FMCSA Trucking - Hazmat Prospects", + "name": "Hazmat Carrier Compliance Campaign", "headline": "Hazmat Compliance Alert", + "subject": "Hazmat compliance review for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Hazmat registration — carriers transporting regulated hazardous materials may need PHMSA/HM registration.", + "Higher insurance and safety exposure — hazmat operations often trigger stricter insurance and audit requirements.", + "Driver and security records — training, endorsements, and emergency response documents need to be audit-ready.", + ], + "services": [("Hazmat Compliance Review", "$199"), ("Safety Audit Prep", "$299"), ("Insurance / State Filing Review", "$99"), ("DOT Full Compliance Bundle", "$599")], + "cta_url": "https://performancewest.net/services/trucking/hazmat/", "cta_text": "Review Hazmat Compliance", + }, + { + "slug": "new-carrier-startup", "list": "FMCSA Trucking - New Carrier Prospects", + "name": "New Carrier Startup Checklist Campaign", "headline": "New Carrier Startup Checklist", + "subject": "New DOT number? Startup checklist for {{ .Subscriber.Attribs.company }}", + "requirements": [ + "Startup filing sequence — LLC, EIN, USDOT, operating authority, BOC-3, UCR, D&A, insurance, IRP and IFTA can all stack up quickly.", + "Missed renewals — new carriers often miss UCR, MCS-150 timing, state permits, or quarterly fuel tax obligations.", + "Cash-flow friendly setup — bundling startup compliance prevents piecemeal filings and duplicate work.", + ], + "services": [("New Carrier Starter Bundle", "$599"), ("LLC Formation", "$199"), ("BOC-3", "$49"), ("UCR Filing", "$99")], + "cta_url": "https://performancewest.net/services/trucking/new-carrier/", "cta_text": "Start Carrier Compliance", + }, +] + + +def create_campaign(cfg: dict, list_id: int) -> int: + payload = { + "name": cfg["name"], + "subject": cfg["subject"], + "from_email": FROM_EMAIL, + "type": "regular", + "content_type": "html", + "body": build_email(cfg), + "lists": [list_id], + "template_id": 1, + "headers": [{"Reply-To": REPLY_TO}], + "tags": ["dot-compliance", "trucking", cfg["slug"]], + "messenger": "email", + } + resp = api("/campaigns", payload, "POST") + return int(resp["data"]["id"]) + + +def send_test(campaign_id: int, cfg: dict, list_id: int) -> None: + body = build_email(cfg) + sample = { + "name": cfg["name"], + "subject": "[TEST] " + cfg["subject"].replace("{{ .Subscriber.Attribs.company }}", "Sample Carrier LLC"), + "from_email": FROM_EMAIL, + "type": "regular", + "content_type": "html", + "body": body, + "lists": [list_id], + "template_id": 1, + "headers": [{"Reply-To": REPLY_TO}], + "tags": ["dot-compliance", "trucking", cfg["slug"], "test"], + "messenger": "email", + "subscribers": [TEST_EMAIL], + } + api(f"/campaigns/{campaign_id}/test", sample, "POST") + + +def main() -> None: + print(f"Listmonk: {LISTMONK_URL}") + print(f"Sending tests to: {TEST_EMAIL}") + created: list[tuple[str, int, int]] = [] + for cfg in CAMPAIGNS: + list_id = get_or_create_list(cfg["list"], ["trucking", "dot-compliance", cfg["slug"]]) + campaign_id = create_campaign(cfg, list_id) + send_test(campaign_id, cfg, list_id) + created.append((cfg["name"], campaign_id, list_id)) + print(f"created draft campaign {campaign_id} on list {list_id}: {cfg['name']} | test sent") + print("\nSummary:") + for name, campaign_id, list_id in created: + print(f"- #{campaign_id} list #{list_id}: {name}") + + +if __name__ == "__main__": + main()