From a2665c22c2ec557dde475fdcfa7457b715f70580 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 14 Jun 2026 00:30:23 -0500 Subject: [PATCH] ucr: annual-renewal reminder campaign + order-alert campaign source UCR (Unified Carrier Registration) is annual: opens Oct 1, due Dec 31, mandatory for interstate carriers (op A, same ~628k pool as IFTA) -> recurring revenue. - build_ucr_annual_campaign.py: 3-touch business-day cadence (30/12/4 bd before Dec 31, wider than IFTA since it's once a year), escalating tone, same-day coupon, 'I already did it' suppression. Reuses build_trucking_campaigns + IFTA business-day/token helpers (DRY). Per-year cycle reset. - ucr_annual_reminder.html: deadline + fines/OOS risk + 'we figure out your fee tier' + coupon + filed link + CAN-SPAM. Source campaign 473. - migration 096: ucr_reminded_at / ucr_touch_no / ucr_self_filed_at. - ifta.ts: add GET /api/v1/ucr/filed (shares the HMAC token scheme). - checkout.ts: order-placement Telegram now shows 'Source: campaign (code X)' when a discount code is present, so IFTA/UCR/CLIA conversions are visible. (Confirmed order-alert Telegram already fires from handlePaymentComplete for all compliance orders via both webhook + session paths.) --- api/migrations/096_fmcsa_ucr_reminder.sql | 15 ++ api/src/routes/checkout.ts | 7 + api/src/routes/ifta.ts | 42 +++ .../ucr_annual_reminder.html | 43 +++ scripts/build_ucr_annual_campaign.py | 249 ++++++++++++++++++ 5 files changed, 356 insertions(+) create mode 100644 api/migrations/096_fmcsa_ucr_reminder.sql create mode 100644 data/trucking_campaigns/ucr_annual_reminder.html create mode 100644 scripts/build_ucr_annual_campaign.py diff --git a/api/migrations/096_fmcsa_ucr_reminder.sql b/api/migrations/096_fmcsa_ucr_reminder.sql new file mode 100644 index 0000000..d11ee6e --- /dev/null +++ b/api/migrations/096_fmcsa_ucr_reminder.sql @@ -0,0 +1,15 @@ +-- UCR annual-renewal reminder tracking (mirrors IFTA): per-carrier touch number, +-- last-touch timestamp, and "I already did it" self-filed suppression. +-- Reset each year by build_ucr_annual_campaign.py. +-- ucr_reminded_at : timestamp of the most recent UCR touch +-- ucr_touch_no : highest touch number sent this cycle (1=30bd,2=12bd,3=4bd) +-- ucr_self_filed_at: clicked "I already registered" -> stop reminding this cycle + +ALTER TABLE fmcsa_carriers + ADD COLUMN IF NOT EXISTS ucr_reminded_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS ucr_touch_no SMALLINT, + ADD COLUMN IF NOT EXISTS ucr_self_filed_at TIMESTAMPTZ; + +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fmcsa_carriers_ucr_touch + ON fmcsa_carriers (ucr_touch_no) + WHERE carrier_operation = 'A'; diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts index b3f869d..d977c23 100644 --- a/api/src/routes/checkout.ts +++ b/api/src/routes/checkout.ts @@ -1735,11 +1735,18 @@ export async function handlePaymentComplete( const idLine = dotNumber ? `DOT#: ${dotNumber}\n` : frn ? `FRN: ${frn}\n` : ""; + // Campaign-source hint: an order carrying the daily campaign coupon (or any + // discount code) almost certainly came from an email campaign. Surface it so + // you can see the IFTA/UCR/CLIA/cold-email pipelines actually converting. + const srcLine = order.discount_code + ? `Source: campaign (code ${order.discount_code})\n` + : ""; const msg = `💰 NEW ORDER\n\n` + `Customer: ${customerName}\n` + `Email: ${customerEmail}\n` + idLine + serviceLine + + srcLine + subtotalLine + discountLine + surchargeLine diff --git a/api/src/routes/ifta.ts b/api/src/routes/ifta.ts index 172908b..9064688 100644 --- a/api/src/routes/ifta.ts +++ b/api/src/routes/ifta.ts @@ -82,4 +82,46 @@ router.get("/api/v1/ifta/filed", async (req, res) => { See how it works →

`)); }); +/** + * One-click "I already did it" for UCR annual reminders. + * GET /api/v1/ucr/filed?dot=1234567&t= (same HMAC token scheme as IFTA) + */ +router.get("/api/v1/ucr/filed", async (req, res) => { + const dot = String(req.query.dot || "").trim(); + const token = String(req.query.t || "").trim(); + res.set("Content-Type", "text/html; charset=utf-8"); + + if (!dot || !token) { + res.status(400).send(page("Invalid link", + `

That link looks incomplete.

+

If you already registered your UCR, you can ignore the reminders. Questions? Call (888) 411-0383.

`)); + return; + } + const expected = iftaFiledToken(dot); // shared token scheme + const ok = token.length === expected.length + && crypto.timingSafeEqual(Buffer.from(token), Buffer.from(expected)); + if (!ok) { + res.status(403).send(page("Invalid link", + `

We couldn't verify that link.

+

If you already registered your UCR, you can ignore the reminders. Questions? Call (888) 411-0383.

`)); + return; + } + try { + await pool.query( + `UPDATE fmcsa_carriers + SET ucr_self_filed_at = COALESCE(ucr_self_filed_at, now()) + WHERE dot_number = $1`, + [dot], + ); + } catch (err) { + console.error("[ucr/filed] db error:", err); + } + res.send(page("Thanks - you're all set", + `

Got it - thanks for letting us know.

+

We'll stop reminding you about this year's UCR for DOT #${dot}. + We'll check back when next year's registration opens.

+

Want us to handle next year's UCR so you don't have to? + See how it works →

`)); +}); + export default router; diff --git a/data/trucking_campaigns/ucr_annual_reminder.html b/data/trucking_campaigns/ucr_annual_reminder.html new file mode 100644 index 0000000..67c3ad1 --- /dev/null +++ b/data/trucking_campaigns/ucr_annual_reminder.html @@ -0,0 +1,43 @@ +
+
+ Performance West +
+
+

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

+

{{ .Subscriber.Attribs.ucr_headline }}

+

{{ .Subscriber.Attribs.ucr_urgency }}

+

Your {{ .Subscriber.Attribs.ucr_year }} Unified Carrier Registration is due by {{ .Subscriber.Attribs.ucr_due_date }}. If you run interstate, UCR is mandatory. Skip it and you face:

+
    +
  • Fines from $100 to $5,000 depending on the state
  • +
  • Being placed out of service at the roadside or a weigh station
  • +
  • Held loads and lost revenue while you sort it out
  • +
+{{ if .Subscriber.Attribs.coupon_code }} +
+

TODAY ONLY - {{ .Subscriber.Attribs.coupon_pct }}% OFF

+

We file your UCR for $39 $23 + the state fee.

+

Use code {{ .Subscriber.Attribs.coupon_code }} at checkout.

+

Expires {{ .Subscriber.Attribs.coupon_expires }}.

+
+{{ else }} +
+

We file your UCR for $39 + the state fee.

+

Two minutes of your time, we handle the rest. No portals, no guesswork on your fee tier.

+
+{{ end }} +
+

We figure out your exact fee tier.

+

UCR fees are based on your fleet size, and getting the tier wrong causes rejections and delays. Tell us your power-unit count and we file it correctly the first time, so you stay legal and on the road.

+
+
+10-4 - File My UCR Now → +
+

Or call us directly at (888) 411-0383.

+
+

Already registered for this year?

+I already did it - stop reminding me +
+

Performance West Inc.
DOT Compliance Services

+
+
performancewest.net · (888) 411-0383
Gotta hit a 10-100 and pull off this channel? Unsubscribe here.
Performance West Inc. · 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001
+
diff --git a/scripts/build_ucr_annual_campaign.py b/scripts/build_ucr_annual_campaign.py new file mode 100644 index 0000000..b3b7efd --- /dev/null +++ b/scripts/build_ucr_annual_campaign.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +"""Build the UCR (Unified Carrier Registration) annual-renewal reminder campaign. + +UCR runs on a FIXED annual calendar: registration opens Oct 1 and is due by +Dec 31 every year. Nearly every interstate carrier (FMCSA op code 'A') must +renew annually -> recurring revenue, no per-carrier data needed. + +Multi-touch escalating cadence (business days before Dec 31), with the same +'I already did it' suppression + same-day coupon as the IFTA reminder. Reuses +the build_trucking_campaigns plumbing for one source of truth. + +Cron (run daily; self-gates to the touch windows): + python3 scripts/build_ucr_annual_campaign.py --start-campaign +Manual / preview: + python3 scripts/build_ucr_annual_campaign.py --preview + python3 scripts/build_ucr_annual_campaign.py --date 2026-11-17 --dry-run +""" +from __future__ import annotations + +import argparse +import os +import sys +from datetime import date, datetime, timedelta, timezone + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if ROOT not in sys.path: + sys.path.insert(0, ROOT) + +import psycopg2 # noqa: E402 + +import scripts.build_trucking_campaigns as tc # noqa: E402 +# Reuse the IFTA helpers (business-day math + HMAC filed token) so there is one +# implementation of each. +import scripts.build_ifta_quarterly_campaign as ifta # noqa: E402 + +LOG = tc.LOG + +UCR_SVC_SLUG = "ucr-registration" +LP_LINK = f"{tc.SITE_DOMAIN}/order/{UCR_SVC_SLUG}" +SOURCE_ENV = "CAMPAIGN_UCR_ANNUAL_ID" + +# UCR is annual (registration opens Oct 1, due Dec 31). Because it is once a year +# we spread the touches a bit wider than IFTA's quarterly cadence. +TOUCHES = [ + # (business_days_before_due, touch_no, subject_suffix, headline, urgency) + (30, 1, "is open - let us file it for you", + "{year} UCR registration is open.", + "UCR opened for {year}. Knocking it out early means it is done and you never think about it again - we can file it for you in a couple minutes of your time."), + (12, 2, "is due soon - still time for us to file", + "Your {year} UCR is due {due}.", + "The UCR deadline is coming up. If you would rather not deal with the fee tiers and the portal, there is still time for us to file it for you - but the window is closing."), + (4, 3, "is due in days - last chance to avoid being put out of service", + "Almost out of time: your {year} UCR is due {due}.", + "This is about the last point we can still get your UCR filed before the deadline. Run interstate without it after Jan 1 and you risk fines and being placed out of service. Let us handle it today."), +] +REMINDER_WINDOW_DAYS = int(os.getenv("UCR_REMINDER_WINDOW_DAYS", "1")) + + +def ucr_due(year: int) -> date: + return date(year, 12, 31) + + +def next_due(today: date) -> tuple[int, date]: + """The UCR year currently being registered + its Dec 31 due date. + + Oct 1 .. Dec 31 -> this year's registration (due this Dec 31). + Jan 1 .. Sep 30 -> the upcoming year's registration is not open yet; we look + ahead to this year's Dec 31 (touches simply will not fire until ~30 bdays out). + """ + due = ucr_due(today.year) + if today > due: + due = ucr_due(today.year + 1) + return due.year, due + + +def due_touch_today(today: date): + year, due = next_due(today) + if not ifta._is_business_day(today): + return None, (year, due) + for touch in TOUCHES: + send_on = ifta.minus_business_days(due, touch[0]) + if abs((today - send_on).days) <= REMINDER_WINDOW_DAYS: + return touch, (year, due) + return None, (year, due) + + +def _reset_cycle_if_new(conn, year: int) -> None: + cycle_key = f"UCR-{year}" + cur = conn.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS ucr_reminder_cycle ( + id boolean PRIMARY KEY DEFAULT true, + cycle_key text NOT NULL, + reset_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ucr_reminder_cycle_singleton CHECK (id) + ) + """) + cur.execute("SELECT cycle_key FROM ucr_reminder_cycle WHERE id = true") + row = cur.fetchone() + if row and row[0] == cycle_key: + conn.commit() + return + cur.execute(""" + UPDATE fmcsa_carriers + SET ucr_reminded_at = NULL, ucr_touch_no = NULL, ucr_self_filed_at = NULL + WHERE ucr_touch_no IS NOT NULL OR ucr_self_filed_at IS NOT NULL + """) + cleared = cur.rowcount + cur.execute(""" + INSERT INTO ucr_reminder_cycle (id, cycle_key, reset_at) + VALUES (true, %s, now()) + ON CONFLICT (id) DO UPDATE SET cycle_key = EXCLUDED.cycle_key, reset_at = now() + """, (cycle_key,)) + conn.commit() + LOG.info("[ucr] new cycle %s -- cleared %d prior marks", cycle_key, cleared) + + +SELECT_SQL = f""" + SELECT dot_number, email_address, legal_name, phy_state + FROM fmcsa_carriers + WHERE carrier_operation = 'A' -- interstate => needs UCR + AND email_address IS NOT NULL AND email_address <> '' + AND {tc.USABLE_FILTER} + AND lower(split_part(email_address, '@', 2)) <> ALL(%s) + AND ucr_self_filed_at IS NULL + AND COALESCE(ucr_touch_no, 0) < %s + ORDER BY dot_number + LIMIT %s +""" + + +def _filed_link(dot: str) -> str: + api_base = os.getenv("PUBLIC_API_URL", "https://api.performancewest.net").rstrip("/") + # Reuse the same HMAC token scheme; the UCR handler verifies it the same way. + return f"{api_base}/api/v1/ucr/filed?dot={dot}&t={ifta._ifta_filed_token(dot)}" + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--date") + ap.add_argument("--start-campaign", action="store_true") + ap.add_argument("--preview", action="store_true") + ap.add_argument("--dry-run", action="store_true") + ap.add_argument("--limit", type=int, default=int(os.getenv("UCR_DAILY_CAP", "2000"))) + ap.add_argument("--ignore-window", action="store_true") + args = ap.parse_args() + + import logging + logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"), + format="%(asctime)s %(levelname)s %(message)s") + + today = datetime.strptime(args.date, "%Y-%m-%d").date() if args.date else date.today() + + touch, (year, due) = due_touch_today(today) + if touch is None and not args.ignore_window and not args.preview: + LOG.info("[ucr] no touch fires today (next due %s); exiting", due) + return 0 + if touch is None: + touch = TOUCHES[0] + days_before, touch_no, subj_suffix, headline_t, urgency_t = touch + + src = os.getenv(SOURCE_ENV) + if not src: + LOG.error("[ucr] %s not set -- create the UCR source campaign first", SOURCE_ENV) + return 2 + base = tc.get_base_campaign(int(src)) + + conn = psycopg2.connect(tc.DB_URL) + if args.start_campaign and not args.preview and not args.dry_run: + _reset_cycle_if_new(conn, year) + + coupon = None + if args.start_campaign and not args.preview and not args.dry_run: + try: + coupon = tc.get_or_create_daily_coupon(conn, today) + except Exception as exc: # noqa: BLE001 + LOG.warning("[ucr] coupon mint failed: %s", exc) + + cur = conn.cursor() + cur.execute(SELECT_SQL, [list(tc.BLOCKED_EMAIL_DOMAINS), touch_no, args.limit]) + rows = cur.fetchall() + LOG.info("[ucr] %d UCR due %s | touch %d (%d biz-days) | %d candidates", + year, due, touch_no, days_before, len(rows)) + if not rows and not args.preview: + LOG.info("[ucr] no candidates for touch %d; exiting", touch_no) + return 0 + + due_human = due.strftime("%B %-d, %Y") + headline = headline_t.format(year=year, due=due_human) + urgency = urgency_t.format(year=year, due=due_human) + + def attribs(dot, company, state): + lp = f"{LP_LINK}?code={coupon}" if coupon else LP_LINK + a = { + "dot_number": dot or "", "company": company or "", "state": state or "", + "lp_link": lp, + "ucr_due_date": due_human, "ucr_year": str(year), + "ucr_headline": headline, "ucr_urgency": urgency, + "filed_link": _filed_link(dot) if dot else "", + } + a.update(tc.coupon_attribs(coupon)) + return a + + if args.preview: + subs = [{"email": tc.TEST_EMAIL, "name": "Sample Carrier", + "attribs": attribs("0000000", "Sample Carrier LLC", "TX")}] + else: + subs = [{"email": r[1], "name": r[2] or r[1], + "attribs": attribs(r[0], r[2], r[3])} for r in rows] + + send_date = today.isoformat() + list_name = f"UCR {year} T{touch_no} - {send_date}" + campaign_name = f"UCR Reminder {year} - Touch {touch_no} ({days_before}bd) - {today.strftime('%b %d %Y')}" + + if args.dry_run: + LOG.info("[ucr] DRY RUN -- touch %d would send to %d carriers (coupon=%s)", + touch_no, len(subs), coupon) + return 0 + + base = dict(base) + base["subject"] = f"DOT# {{{{ .Subscriber.Attribs.dot_number }}}} - Your {year} UCR registration {subj_suffix}" + + list_id = tc.create_list(list_name) + added = tc.import_subscribers(list_id, subs) + LOG.info("[ucr] list %d: %d/%d subscribers added", list_id, added, len(subs)) + if added == 0: + LOG.error("[ucr] 0 subscribers added; aborting") + return 1 + + send_at = datetime.now(timezone.utc) + timedelta(minutes=5) + cid = tc.create_and_schedule_campaign( + base, list_id, campaign_name, send_at, schedule=args.start_campaign and not args.preview) + LOG.info("[ucr] campaign %d created (%s)", cid, + "scheduled" if args.start_campaign and not args.preview else "draft") + + if args.start_campaign and not args.preview and rows: + cur.execute(""" + UPDATE fmcsa_carriers SET ucr_reminded_at = now(), ucr_touch_no = %s + WHERE dot_number = ANY(%s::text[]) + """, (touch_no, [r[0] for r in rows])) + conn.commit() + LOG.info("[ucr] marked %d carriers at touch %d", len(rows), touch_no) + + conn.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())