#!/usr/bin/env python3 """Create the 6 Listmonk *source* campaigns for the deficiency-flag segments. These are draft templates that build_trucking_campaigns.py clones for each timezone blast (subject + body + template_id are copied). They are created as DRAFTS (never scheduled) and live in the same Listmonk install. After running, the script prints the `CAMPAIGN_*_ID` env lines to paste into the workers service env. Idempotent-ish: pass --dry-run to preview, and it skips a segment if a campaign with the same name already exists (prints its id instead). Usage (inside the workers container on prod): python3 scripts/create_deficiency_source_campaigns.py [--dry-run] """ from __future__ import annotations import argparse import sys import build_trucking_campaigns as b # reuse lm_api + auth + segment defs FROM_EMAIL = "Performance West " TEMPLATE_ID = 6 # same wrapper template as the MCS-150 source campaign PHONE = "(888) 411-0383" PHONE_TEL = "8884110383" # Shared chrome (header + footer). The middle is segment-specific. _HEADER = ( '
' '
' 'Performance West' '
' '
' '

{{ .Subscriber.Attribs.company }},
' 'DOT# {{ .Subscriber.Attribs.dot_number }}

' ) _FOOTER = ( '

Or call us directly at ' f'{PHONE}.

' '

Performance West Inc.
DOT Compliance Services

' '
' '
' 'performancewest.net · ' f'{PHONE}
' '
' 'Gotta hit a 10-100 and pull off this channel? ' 'Unsubscribe here.
' 'Performance West Inc. · 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001
' '
' ) def _alert(color_border, color_text, lines_html): return ( f'
{lines_html}
' ) def _price_box(headline, sub): return ( '
' f'

{headline}

' f'

{sub}

' ) def _cta(label): # Clicks land on the per-subscriber order page (resolved per row, incl. # per-state overrides) via the lp_link attrib, with UTM tracking. return ( '
' '{label} →
' ) def _body(headline, intro, bullets, price_headline, price_sub, cta_label): bullet_html = "".join(f"
  • {x}
  • " for x in bullets) alert = _alert( "#fca5a5", "#991b1b", f'

    {headline}

    ' f'

    {intro}

    ' f'' ) reassure = ( '
    ' '

    Not tech savvy? No problem.

    ' '

    One simple form, from your phone or ' 'computer. No Login.gov, no government portals, no hours on hold. We handle the paperwork so you can get ' 'back to trucking and making money.

    ' ) return _HEADER + alert + _price_box(price_headline, price_sub) + reassure + _cta(cta_label) + _FOOTER # ── Per-segment copy ──────────────────────────────────────────────────────── SEGMENTS = { "for_hire_boc3": { "env": "CAMPAIGN_FOR_HIRE_ID", "name": "For-Hire Carrier — BOC-3 + UCR Required", "subject": "DOT# {{ .Subscriber.Attribs.dot_number }} - For-hire carriers must file BOC-3 & UCR", "headline": "You're operating for-hire without a BOC-3 process agent on file.", "intro": "Our records show you run for-hire authority but are missing required filings. Without them you face:", "bullets": [ "Authority suspension by the FMCSA", "UCR penalties of up to $5,000", "Loads refused at the scale and by brokers", ], "price_headline": "We file your BOC-3 process agents nationwide.", "price_sub": "Plus UCR registration if you need it. We handle everything.", "cta": "Fix My BOC-3 & UCR", }, "irp_ifta": { "env": "CAMPAIGN_IRP_IFTA_ID", "name": "Interstate Carrier — IRP / IFTA Registration", "subject": "DOT# {{ .Subscriber.Attribs.dot_number }} - You need IRP plates & an IFTA license", "headline": "Running interstate without IRP apportioned plates or an IFTA license?", "intro": "If you cross state lines over 26,000 lbs you must be registered. Missing it means:", "bullets": [ "Fines and citations at every weigh station", "Trip permits that cost far more than apportioned plates", "Quarterly IFTA penalties and interest", ], "price_headline": "We set up your IRP plates and IFTA license.", "price_sub": "Base-state registration done for you, no DMV lines.", "cta": "Get IRP & IFTA Done", }, "intrastate_authority": { "env": "CAMPAIGN_INTRASTATE_ID", "name": "Intrastate Operating Authority Required", "subject": "DOT# {{ .Subscriber.Attribs.dot_number }} - Your state requires intrastate authority", "headline": "You're hauling inside your state without intrastate operating authority.", "intro": "Most states require their own authority on top of your USDOT. Without it:", "bullets": [ "Out-of-service orders at state inspections", "Citations and impound risk on intrastate loads", "Insurance filings rejected by the state", ], "price_headline": "We file your state intrastate authority.", "price_sub": "Application, insurance filings, and BOC-3 if required.", "cta": "Get My State Authority", }, "state_weight_tax": { "env": "CAMPAIGN_WEIGHT_TAX_ID", "name": "State Weight-Distance Tax Registration", "subject": "DOT# {{ .Subscriber.Attribs.dot_number }} - You owe a state weight-distance tax", "headline": "You operate in a state with a weight-distance / highway-use tax.", "intro": "OR, NY, KY, NM and CT charge a per-mile tax that requires its own account. Skipping it means:", "bullets": [ "Back taxes plus penalties and interest", "Permits pulled at the border", "Audit exposure on every mile run unregistered", ], "price_headline": "We register your weight-distance tax account.", "price_sub": "OR WMT, NY HUT, KY KYU, NM WDT, or CT HUF, done for you.", "cta": "Register My Tax Account", }, "state_emissions": { "env": "CAMPAIGN_EMISSIONS_ID", "name": "State Clean-Truck / Emissions Compliance", "subject": "DOT# {{ .Subscriber.Attribs.dot_number }} - New clean-truck rules apply to you", "headline": "Your state has new clean-truck / emissions reporting you may be missing.", "intro": "CA, NY, CO, MD, NJ and MA now require fleet emissions registration. Non-compliance means:", "bullets": [ "Registration holds on affected vehicles", "Penalties under state clean-truck (ACT) rules", "Loss of access to ports and regulated lanes", ], "price_headline": "We handle your clean-truck / emissions registration.", "price_sub": "CARB, ACT and state reporting set up for your fleet.", "cta": "Check My Emissions Status", }, "hazmat": { "env": "CAMPAIGN_HAZMAT_ID", "name": "Hazmat PHMSA Registration Required", "subject": "DOT# {{ .Subscriber.Attribs.dot_number }} - Hazmat haulers must register with PHMSA", "headline": "You haul hazardous materials but may be missing PHMSA registration.", "intro": "Carriers of placardable hazmat must register annually with PHMSA. Without it:", "bullets": [ "Civil penalties up to $89,678 per violation", "Loads refused by shippers who verify your registration", "Out-of-service orders for unregistered hazmat transport", ], "price_headline": "We file your PHMSA hazmat registration.", "price_sub": "Annual registration handled, government fee billed at cost.", "cta": "Register My Hazmat", }, } def find_existing(name: str) -> int | None: try: res = b.lm_api("/campaigns?query=" + name.replace(" ", "+") + "&per_page=100") for c in res.get("data", {}).get("results", []): if c.get("name") == name: return c["id"] except Exception: pass return None def create_draft(seg_key: str, cfg: dict, dry: bool) -> int | None: existing = find_existing(cfg["name"]) if existing: print(f" [{seg_key}] already exists -> id {existing} (skipping create)") return existing body = _body(cfg["headline"], cfg["intro"], cfg["bullets"], cfg["price_headline"], cfg["price_sub"], cfg["cta"]) if dry: print(f" [{seg_key}] DRY-RUN would create '{cfg['name']}' (body {len(body)} chars)") return None payload = { "name": cfg["name"], "subject": cfg["subject"], "lists": [1], # placeholder list; clones override with the real per-TZ list "from_email": FROM_EMAIL, "type": "regular", "content_type": "html", "body": body, "template_id": TEMPLATE_ID, "tags": ["trucking", "deficiency", "source"], "messenger": "email", } res = b.lm_api("/campaigns", payload, "POST") cid = res["data"]["id"] print(f" [{seg_key}] created '{cfg['name']}' -> id {cid}") return cid def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() print(f"Listmonk: {b.LISTMONK_URL}") print(f"Creating {len(SEGMENTS)} deficiency source campaigns " f"({'DRY-RUN' if args.dry_run else 'LIVE'}):\n") env_lines = [] for key, cfg in SEGMENTS.items(): cid = create_draft(key, cfg, args.dry_run) if cid: env_lines.append(f"{cfg['env']}={cid}") if env_lines: print("\n=== Add these to the workers service environment ===") for line in env_lines: print(line) return 0 if __name__ == "__main__": sys.exit(main())