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.)
This commit is contained in:
parent
2b361a83a8
commit
a2665c22c2
5 changed files with 356 additions and 0 deletions
15
api/migrations/096_fmcsa_ucr_reminder.sql
Normal file
15
api/migrations/096_fmcsa_ucr_reminder.sql
Normal file
|
|
@ -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';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -82,4 +82,46 @@ router.get("/api/v1/ifta/filed", async (req, res) => {
|
|||
<a href="https://performancewest.net/order/ifta-quarterly" style="color:#0f766e;font-weight:700">See how it works →</a></p>`));
|
||||
});
|
||||
|
||||
/**
|
||||
* One-click "I already did it" for UCR annual reminders.
|
||||
* GET /api/v1/ucr/filed?dot=1234567&t=<token> (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",
|
||||
`<h2 style="color:#b91c1c">That link looks incomplete.</h2>
|
||||
<p style="color:#475569">If you already registered your UCR, you can ignore the reminders. Questions? Call (888) 411-0383.</p>`));
|
||||
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",
|
||||
`<h2 style="color:#b91c1c">We couldn't verify that link.</h2>
|
||||
<p style="color:#475569">If you already registered your UCR, you can ignore the reminders. Questions? Call (888) 411-0383.</p>`));
|
||||
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",
|
||||
`<h2 style="color:#0f766e">Got it - thanks for letting us know.</h2>
|
||||
<p style="color:#475569;line-height:1.6">We'll stop reminding you about this year's UCR for DOT #${dot}.
|
||||
We'll check back when next year's registration opens.</p>
|
||||
<p style="color:#475569;line-height:1.6">Want us to handle next year's UCR so you don't have to?
|
||||
<a href="https://performancewest.net/order/ucr-registration" style="color:#0f766e;font-weight:700">See how it works →</a></p>`));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
43
data/trucking_campaigns/ucr_annual_reminder.html
Normal file
43
data/trucking_campaigns/ucr_annual_reminder.html
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<div style="font-family:-apple-system,system-ui,sans-serif;max-width:600px;margin:0 auto">
|
||||
<div style="background:#1a2744;padding:24px;text-align:center;border-radius:12px 12px 0 0">
|
||||
<img src="https://performancewest.net/images/logo.png" alt="Performance West" style="height:40px">
|
||||
</div>
|
||||
<div style="background:#fff;border:1px solid #e2e8f0;padding:32px;border-radius:0 0 12px 12px">
|
||||
<p style="font-size:15px;color:#374151;line-height:1.6">{{ .Subscriber.Attribs.company }},<br>DOT# {{ .Subscriber.Attribs.dot_number }}</p>
|
||||
<h1 style="font-size:22px;color:#dc2626;margin:16px 0">{{ .Subscriber.Attribs.ucr_headline }}</h1>
|
||||
<p style="font-size:15px;color:#374151;line-height:1.6">{{ .Subscriber.Attribs.ucr_urgency }}</p>
|
||||
<p style="font-size:15px;color:#374151;line-height:1.6">Your <strong>{{ .Subscriber.Attribs.ucr_year }} Unified Carrier Registration</strong> is due by <strong>{{ .Subscriber.Attribs.ucr_due_date }}</strong>. If you run interstate, UCR is mandatory. Skip it and you face:</p>
|
||||
<ul style="font-size:15px;color:#374151;line-height:1.8">
|
||||
<li><strong>Fines from $100 to $5,000</strong> depending on the state</li>
|
||||
<li>Being <strong>placed out of service</strong> at the roadside or a weigh station</li>
|
||||
<li>Held loads and lost revenue while you sort it out</li>
|
||||
</ul>
|
||||
{{ if .Subscriber.Attribs.coupon_code }}
|
||||
<div style="background:#fff7ed;border:2px solid #f97316;border-radius:10px;padding:20px;margin:20px 0;text-align:center">
|
||||
<p style="font-size:13px;font-weight:700;color:#9a3412;letter-spacing:.04em;margin:0 0 6px">TODAY ONLY - {{ .Subscriber.Attribs.coupon_pct }}% OFF</p>
|
||||
<p style="font-size:18px;font-weight:700;color:#9a3412;margin:0 0 4px">We file your UCR for <span style="text-decoration:line-through;color:#c2410c;font-weight:600">$39</span> <span style="color:#15803d">$23</span> + the state fee.</p>
|
||||
<p style="font-size:14px;color:#9a3412;margin:0 0 4px">Use code <strong style="font-size:16px;letter-spacing:.08em">{{ .Subscriber.Attribs.coupon_code }}</strong> at checkout.</p>
|
||||
<p style="font-size:12px;color:#b91c1c;font-weight:700;margin:0">Expires {{ .Subscriber.Attribs.coupon_expires }}.</p>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div style="background:#f0fdf4;border:2px solid #86efac;border-radius:10px;padding:20px;margin:20px 0;text-align:center">
|
||||
<p style="font-size:18px;font-weight:700;color:#166534;margin:0 0 4px">We file your UCR for $39 + the state fee.</p>
|
||||
<p style="font-size:14px;color:#15803d;margin:0">Two minutes of your time, we handle the rest. No portals, no guesswork on your fee tier.</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:20px 0">
|
||||
<p style="font-size:14px;color:#374151;margin:0 0 6px"><strong>We figure out your exact fee tier.</strong></p>
|
||||
<p style="font-size:13px;color:#64748b;margin:0;line-height:1.6">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.</p>
|
||||
</div>
|
||||
<div style="text-align:center;margin:24px 0">
|
||||
<a href="{{ .Subscriber.Attribs.lp_link }}&utm_source=listmonk&utm_medium=email&utm_campaign=ucr-annual@TrackLink" style="display:inline-block;padding:14px 36px;background:#f97316;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:16px">10-4 - File My UCR Now →</a>
|
||||
</div>
|
||||
<p style="font-size:14px;color:#64748b;line-height:1.6">Or call us directly at <a href="tel:8884110383" style="color:#f97316;font-weight:600">(888) 411-0383</a>.</p>
|
||||
<div style="text-align:center;margin:18px 0 4px;padding-top:14px;border-top:1px solid #e5e7eb">
|
||||
<p style="font-size:13px;color:#94a3b8;margin:0 0 6px">Already registered for this year?</p>
|
||||
<a href="{{ .Subscriber.Attribs.filed_link }}" style="font-size:13px;color:#64748b;text-decoration:underline">I already did it - stop reminding me</a>
|
||||
</div>
|
||||
<p style="font-size:14px;color:#64748b">Performance West Inc.<br>DOT Compliance Services</p>
|
||||
</div>
|
||||
<div style="text-align:center;padding:16px;font-size:11px;color:#94a3b8"><a href="https://performancewest.net" style="color:#94a3b8">performancewest.net</a> · (888) 411-0383</div><div style="text-align:center;padding:0 16px 18px;font-size:11px;color:#94a3b8;line-height:1.7">Gotta hit a 10-100 and pull off this channel? <a href="{{ UnsubscribeURL }}" style="color:#94a3b8;text-decoration:underline">Unsubscribe here</a>.<br>Performance West Inc. · 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001</div>
|
||||
</div>
|
||||
249
scripts/build_ucr_annual_campaign.py
Normal file
249
scripts/build_ucr_annual_campaign.py
Normal file
|
|
@ -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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue