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 @@
+
+
+

+
+
+
{{ .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.
+
+
+
Or call us directly at (888) 411-0383.
+
+
Performance West Inc.
DOT Compliance Services
+
+
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())