diff --git a/scripts/build_trucking_campaigns.py b/scripts/build_trucking_campaigns.py index 659a41f..7e233ca 100644 --- a/scripts/build_trucking_campaigns.py +++ b/scripts/build_trucking_campaigns.py @@ -55,7 +55,7 @@ CAMPAIGN_MCS150_ID = 186 # "MCS-150 Overdue — $1,000/Day Fine Risk" CAMPAIGN_INACTIVE_ID = 188 # "Inactive USDOT — Reactivate Before You Get Pulled Over" # Public site for landing-page links injected into Listmonk subscriber attribs. -SITE_DOMAIN = os.getenv("SITE_DOMAIN", "https://performancewest.com") +SITE_DOMAIN = os.getenv("SITE_DOMAIN", "https://performancewest.net") # ── Deficiency-flag segments (Phase 5) ────────────────────────────────────── # Each segment targets carriers by a `deficiency_flags` value (the TEXT[] column diff --git a/scripts/create_deficiency_source_campaigns.py b/scripts/create_deficiency_source_campaigns.py index 772bb80..29ed8fe 100644 --- a/scripts/create_deficiency_source_campaigns.py +++ b/scripts/create_deficiency_source_campaigns.py @@ -17,6 +17,7 @@ from __future__ import annotations import argparse import os import sys +import urllib.parse # Allow both supported invocation styles: # python -m scripts.create_deficiency_source_campaigns @@ -89,6 +90,25 @@ def _cta(label): ) +def _dot_check_cta(): + """Secondary CTA to the free DOT compliance checker. + + The primary CTA remains the highly specific order page for the detected + deficiency. This second orange button gives carriers a broader way to verify + their DOT status and see any other missing filings before they order. + """ + return ( + '
' + '

' + 'Want to verify everything else on your DOT profile first?

' + f'' + 'Run Free DOT Compliance Check →
' + ) + + def _body(headline, intro, bullets, price_headline, price_sub, cta_label): bullet_html = "".join(f"
  • {x}
  • " for x in bullets) alert = _alert( @@ -104,7 +124,8 @@ def _body(headline, intro, bullets, price_headline, price_sub, cta_label): '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 + return (_HEADER + alert + _price_box(price_headline, price_sub) + reassure + + _cta(cta_label) + _dot_check_cta() + _FOOTER) # ── Per-segment copy ──────────────────────────────────────────────────────── @@ -204,7 +225,7 @@ SEGMENTS = { def find_existing(name: str) -> int | None: try: - res = b.lm_api("/campaigns?query=" + name.replace(" ", "+") + "&per_page=100") + res = b.lm_api("/campaigns?" + urllib.parse.urlencode({"query": name, "per_page": 100})) for c in res.get("data", {}).get("results", []): if c.get("name") == name: return c["id"] @@ -213,13 +234,51 @@ def find_existing(name: str) -> int | None: 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 +def configured_campaign_id(cfg: dict) -> int | None: + raw = os.getenv(cfg["env"], "").strip() + if not raw: + return None + try: + return int(raw) + except ValueError: + print(f" [{cfg['env']}] invalid configured campaign id {raw!r}; falling back to name lookup") + return None + + +def update_existing_campaign(campaign_id: int, cfg: dict, body: str, dry: bool) -> None: + """Update an existing source draft body/metadata so future clones inherit it.""" + existing = b.get_base_campaign(campaign_id) + payload = { + "name": existing.get("name") or cfg["name"], + "subject": cfg["subject"], + "lists": [l["id"] for l in existing.get("lists", []) if isinstance(l, dict)] or [SOURCE_LIST_ID], + "from_email": existing.get("from_email") or FROM_EMAIL, + "type": existing.get("type") or "regular", + "content_type": existing.get("content_type") or "html", + "body": body, + "altbody": existing.get("altbody"), + "template_id": existing.get("template_id") or TEMPLATE_ID, + "tags": existing.get("tags") or ["trucking", "deficiency", "source"], + "messenger": existing.get("messenger") or "email", + "headers": existing.get("headers") or [{"name": "Reply-To", "value": REPLY_TO}], + } + if dry: + print(f" [{cfg['env']}] DRY-RUN would update source campaign {campaign_id} (body {len(body)} chars)") + return + b.lm_api(f"/campaigns/{campaign_id}", payload, "PUT") + print(f" [{cfg['env']}] updated source campaign {campaign_id}") + + +def create_draft(seg_key: str, cfg: dict, dry: bool, update_existing: bool = False) -> int | None: + existing = configured_campaign_id(cfg) or find_existing(cfg["name"]) body = _body(cfg["headline"], cfg["intro"], cfg["bullets"], cfg["price_headline"], cfg["price_sub"], cfg["cta"]) + if existing: + if update_existing: + update_existing_campaign(existing, cfg, body, dry) + else: + print(f" [{seg_key}] already exists -> id {existing} (skipping create)") + return existing if dry: print(f" [{seg_key}] DRY-RUN would create '{cfg['name']}' (body {len(body)} chars)") return None @@ -245,14 +304,20 @@ def create_draft(seg_key: str, cfg: dict, dry: bool) -> int | None: def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") + ap.add_argument( + "--update-existing", + action="store_true", + help="Update matching existing source draft campaigns instead of only reporting their IDs.", + ) 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") + f"({'DRY-RUN' if args.dry_run else 'LIVE'}" + f"{', update existing' if args.update_existing else ''}):\n") env_lines = [] for key, cfg in SEGMENTS.items(): - cid = create_draft(key, cfg, args.dry_run) + cid = create_draft(key, cfg, args.dry_run, args.update_existing) if cid: env_lines.append(f"{cfg['env']}={cid}")