diff --git a/scripts/create_deficiency_source_campaigns.py b/scripts/create_deficiency_source_campaigns.py new file mode 100644 index 0000000..e21e0d9 --- /dev/null +++ b/scripts/create_deficiency_source_campaigns.py @@ -0,0 +1,256 @@ +#!/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())