Compare commits

...

2 commits

Author SHA1 Message Date
justin
5546c58bf0 fix(intake): repair order wizard — checkout was fully broken on trucking/HC
Diagnosed via live browser E2E why campaign clicks (25 checkout-page-views,
36h) produced 0 conversions. Four bugs, all blocking checkout:

1. DOTIntakeStep: a missing `});` (DFWP hydration block, commit 9718ab9
   Jun 2) left the pw:step-shown listener unclosed -> 'missing ) after
   argument list' SYNTAX ERROR killed the whole DOT intake script. Effect:
   ?dot= prefill silently failed for ~3 weeks (exactly the campaign window),
   so every carrier had to re-type all their details.

2. ReviewStep: service slug read from `.pw-step[data-slug]` (first match),
   which on trucking/HC is the INTAKE step's slug ('dot-intake'/'npi-intake'),
   not the order. The cold-visitor order-create POST sent
   service_slug='dot-intake' -> API 501/400 -> 'Could not validate order',
   blocking checkout at the review step on EVERY multi-step vertical. Now
   reads `.pw-wizard[data-service]` (authoritative). Confirmed against prod:
   bad slug=400, correct slug=201.

3. Shared-bundle null derefs: every step's <script> is bundled onto every
   order page, so steps whose anchor element is absent threw at top level and
   could abort siblings:
     - ClassificationWizard: top-level renderQuestion(0) -> appendChild on
       null (errored on 47/67 order pages)
     - BDCDataStep: (querySelector as HTMLElement).getAttribute on null
     - STIRShakenStep / EarthStationStep: top-level addEventListener on null
     - ForeignQualStep: many top-level getElementById(...)! lookups
   Each now guarded to no-op when its step isn't present.

Verified by browser E2E: full flow dot-intake -> review -> payment ->
live Stripe Checkout session, and a 67-page scan now reports 0 JS errors
(was 47 pages erroring). Real human clicks are tracked via Umami; these
were pure functional breakages of the conversion path.
2026-06-23 13:08:41 -05:00
justin
3325259af7 fix(email): drop @TrackLink from per-subscriber CTAs (404 + collapse bug)
Listmonk @TrackLink registers ONE static URL per tracked link and points
every recipient's /link/<uuid> redirect at it. On per-subscriber hrefs
({{ lp_link }}, ?dot=, ?npi=, ?clia=) this is doubly broken:
 - the registered links.url was captured before the {{ lp_link }} token
   rendered, yielding /order/slug&utm_source=... (first &, no ?) -> 404
 - even when valid it collapses every carrier/provider onto the first
   subscriber's dot/npi/clia value

Real human clicks are already tracked via Umami campaign-click (bot
filtered), so Listmonk link tracking here is redundant and destructive.

Stripped @TrackLink from per-subscriber CTAs:
 - scripts/create_deficiency_source_campaigns.py (_cta, _dot_check_cta)
 - data/trucking_campaigns/{ucr,ifta}_*.html
 - data/hc_campaigns/*.html (10 templates)

Static CTAs (e.g. CRTC ?code= order link) keep @TrackLink (safe).
Live fix to the 10 broken registered links.url rows applied separately
(first & -> ?), backup in listmonk.pw_links_dkim_fix_bak_20260622.

Docs: new runbook incident section + corrected the disproven
'use @TrackLink on all CTAs' guidance in fmcsa/hc plans.
2026-06-22 17:01:39 -05:00
23 changed files with 134 additions and 26 deletions

View file

@ -58,7 +58,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">Let us take the CLIA renewal off your plate &mdash; the sooner we start, the better.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.</p>
<a href="https://performancewest.net/order/clia-renewal?clia={{ .Subscriber.Attribs.clia }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-clia-renewal@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Renew my CLIA certificate &rarr;</a>
<a href="https://performancewest.net/order/clia-renewal?clia={{ .Subscriber.Attribs.clia }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-clia-renewal" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Renew my CLIA certificate &rarr;</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -42,7 +42,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">One annual bundle covers your core CMS obligations.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">We watch the deadlines so you never miss one.</p>
<a href="https://performancewest.net/order/provider-compliance-bundle?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-compliance-bundle@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Get the compliance bundle →</a>
<a href="https://performancewest.net/order/provider-compliance-bundle?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-compliance-bundle" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Get the compliance bundle →</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -62,7 +62,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">We handle the CMS-855 reactivation end to end.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">We verify every field against current CMS requirements.</p>
<a href="https://performancewest.net/order/npi-reactivation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-npi-reactivation@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Reactivate my enrollment →</a>
<a href="https://performancewest.net/order/npi-reactivation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-npi-reactivation" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Reactivate my enrollment →</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -62,7 +62,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">Run your free compliance check &mdash; takes about 30 seconds.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">Your NPI is pre-filled. No signup, no cost &mdash; just your results.</p>
<a href="https://performancewest.net/tools/npi-compliance-check?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-npi-check@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Run my free NPI check →</a>
<a href="https://performancewest.net/tools/npi-compliance-check?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-npi-check" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Run my free NPI check →</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -42,7 +42,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">We run and document your OIG/SAM exclusion screening.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">Monthly checks with an audit-ready record &mdash; cancel anytime.</p>
<a href="https://performancewest.net/order/oig-sam-screening?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-oig-screening@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Set up exclusion screening →</a>
<a href="https://performancewest.net/order/oig-sam-screening?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-oig-screening" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Set up exclusion screening →</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -66,7 +66,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">We file your PECOS revalidation for you, well before the deadline.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">Most filings submitted within 1-2 business days.</p>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-due-soon@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Start my revalidation →</a>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-due-soon" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Start my revalidation →</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -66,7 +66,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">We file your PECOS revalidation for you, before the clock runs out.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">Most filings submitted within 1-2 business days.</p>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-overdue@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Start my revalidation →</a>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-overdue" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Start my revalidation →</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -58,7 +58,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">Let us get your past-due revalidation filed right away.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.</p>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-overdue-personal@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Handle my revalidation &rarr;</a>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-overdue-personal" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Handle my revalidation &rarr;</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -57,7 +57,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">Let us take revalidation off your plate &mdash; the sooner we start, the better.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.</p>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-personal@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Handle my revalidation &rarr;</a>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-personal" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Handle my revalidation &rarr;</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -57,7 +57,7 @@
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;"><tr><td style="background:#ecfdf5;border:2px solid #10b981;border-radius:10px;padding:18px;text-align:center;">
<p style="font-size:14px;color:#065f46;margin:0 0 6px;font-weight:600;">Let us take revalidation off your plate &mdash; the sooner we start, the better.</p>
<p style="font-size:12px;color:#047857;margin:0 0 14px;">We submit most filings within 1-2 business days, then track it through CMS processing to confirmation.</p>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-turnover@TrackLink" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Handle my revalidation &rarr;</a>
<a href="https://performancewest.net/order/npi-revalidation?npi={{ .Subscriber.Attribs.npi }}&utm_source=listmonk&utm_medium=email&utm_campaign=hc-reval-turnover" style="display:inline-block;padding:14px 40px;background:#10b981;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px;">Handle my revalidation &rarr;</a>
</td></tr></table>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;"><tr><td style="background:#f0f4f8;border-radius:8px;padding:16px;font-size:13px;color:#374151;line-height:1.6;">

View file

@ -30,7 +30,7 @@
<p style="font-size:13px;color:#64748b;margin:0;line-height:1.6">Send us your total miles and gallons by jurisdiction for the quarter. We calculate the tax owed for every state you ran in, prepare the return, and file it. You just review and we handle the rest - so you can get back to driving.</p>
</div>
<div style="text-align:center;margin:24px 0">
<a href="{{ .Subscriber.Attribs.lp_link }}&utm_source=listmonk&utm_medium=email&utm_campaign=ifta-quarterly@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 IFTA Return &rarr;</a>
<a href="{{ .Subscriber.Attribs.lp_link }}&utm_source=listmonk&utm_medium=email&utm_campaign=ifta-quarterly" 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 IFTA Return &rarr;</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">

View file

@ -30,7 +30,7 @@
<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 &rarr;</a>
<a href="{{ .Subscriber.Attribs.lp_link }}&utm_source=listmonk&utm_medium=email&utm_campaign=ucr-annual" 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 &rarr;</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">

View file

@ -201,6 +201,65 @@ b.old_listmonk_sent_at FROM resend_dkim_backup_20260622 b WHERE c.dot_number =
b.dot_number;`. To resume normal warmup exclusion later, unset
`MAIN_EXCLUDE_OPERATORS` (reverts to Google+Microsoft+consumer-MX held to day 30).
### Incident: Jun 22 2026 — `@TrackLink` on per-subscriber CTAs = 404 + collapse
**Symptom.** The trucking "deficiency" CTA buttons (the primary order link and the
secondary DOT-check link) rendered as Listmonk tracking redirects
(`https://lists.performancewest.net/link/<uuid>/...`) that **404'd**. The redirect
target (registered in `links.url`) was `https://performancewest.net/order/boc3-filing&utm_source=...`
— note the `&` with **no `?`** — an invalid URL.
**Root cause.** Listmonk's `@TrackLink` marker registers **one static URL per
tracked link** and points every recipient's `/link/<uuid>` redirect at that single
row. This is fundamentally incompatible with a **per-subscriber** href such as
`{{ .Subscriber.Attribs.lp_link }}&utm_source=...`:
- The registered `links.url` was captured with the `{{ lp_link }}` token dropped,
yielding `/order/slug&utm_source=...` (first `&`, no `?`) → **404 for everyone**.
- Even if the URL had been valid, a static registration **collapses every carrier
onto the first subscriber's** `?dot=` (or `?npi=`/`?clia=`) value — wrong order
pre-fill for the entire blast.
By contrast, a **static** CTA (same URL for all recipients, e.g. the CRTC
`?code=...` order link) tracks correctly — keep `@TrackLink` there.
**Why removing tracking loses nothing.** Real human clicks are already attributed
via Umami's `campaign-click` event (bot-filtered by `pw-bot-filter.js`). Listmonk's
own click counters were already established as unreliable for this stream. So
Listmonk link tracking on per-subscriber CTAs is both redundant and destructive.
**Fix — live (already-sent + in-flight mail).** Rewrote the 10 broken registered
rows in place (replace the first `&` with `?`) so the baked `/link/<uuid>` redirects
resolve. Backup table `listmonk.pw_links_dkim_fix_bak_20260622` holds the old urls.
Verified the exact redirect that 404'd now returns 200 → lands on the (generic,
DOT-not-prefilled but fully functional) order page. To revert:
```sql
UPDATE links l SET url = b.url
FROM pw_links_dkim_fix_bak_20260622 b WHERE l.id = b.id; -- in the `listmonk` DB
```
**Fix — source (future builds, the real fix).** Stripped `@TrackLink` from every
**per-subscriber / per-provider** CTA so each row renders its own direct link (no
redirect, no collapse). Files changed:
- `scripts/create_deficiency_source_campaigns.py``_cta()` (lp_link order button)
and `_dot_check_cta()` (per-DOT tools link).
- `data/trucking_campaigns/{ucr_annual_reminder,ifta_quarterly_reminder}.html`
(per-carrier `lp_link`).
- `data/hc_campaigns/*.html` (10 templates, per-provider `?npi=`/`?clia=`).
`lp_link` already starts its query with `?dot=` (see `lp_link_with_coupon()`), so
`{{ lp_link }}&utm...` renders to a valid per-carrier URL once the redirect is gone.
**Healthcare note.** The HC Listmonk DB (`listmonk_hc`) had **0 registered links**
despite 13,425 sent — `@TrackLink` was not being stripped there at all, so the
literal `@TrackLink` shipped as harmless trailing text in `utm_campaign` and the
hrefs still 200'd (per-provider `?npi=` was present literally in the template, not
via lp_link). No live HC breakage; source templates cleaned anyway to remove the
collapse risk on the next send.
**Guardrail.** Never put `@TrackLink` on an href containing a `{{ .Subscriber... }}`
token. Per-subscriber links must render directly; rely on Umami `campaign-click`
for human-click attribution.
### Follow-up hardening — DONE (Jun 17-18 2026)
All discovered during the post-incident technical audit; each fix is codified.

View file

@ -137,7 +137,11 @@
- Same Listmonk infrastructure as FCC campaigns
- Warmup schedule (200/day → ramp up)
- Link to DOT compliance checker with `?dot={DOT#}&email={email}` pre-filled
- Use `@TrackLink` on all CTAs (learned from FCC campaign mistake)
- Use `@TrackLink` ONLY on **static** CTAs (same URL for all recipients). NEVER on
a per-subscriber href such as `{{ lp_link }}` / `?dot={DOT#}` — Listmonk registers
one static URL per tracked link and would 404 + collapse every carrier onto one
DOT (see runbook "Jun 22 2026 — @TrackLink on per-subscriber CTAs"). Per-subscriber
links render directly; human clicks are tracked via Umami `campaign-click`.
- Free compliance check as the CTA (not direct sell)
## Phase 5: Automation (Future)

View file

@ -7,8 +7,13 @@ click tracking, and de-risking unsubstantiated status claims.
1. **Removed all service prices** from the emails (price is now revealed on the
order page, after value is established). Catalog (`api/src/service-catalog.ts`)
remains the source of truth.
2. **Fixed click tracking** — appended `@TrackLink` + UTM to every conversion CTA
(root cause of clicks=0; Listmonk only registers links with that marker).
2. **Click tracking** — originally appended `@TrackLink` + UTM to every conversion
CTA. **SUPERSEDED (Jun 22 2026):** `@TrackLink` must NOT be used on per-provider
hrefs (`?npi=`/`?clia=`/`{{ lp_link }}`) — Listmonk registers one static URL per
tracked link, which 404s and collapses every provider onto one NPI. `@TrackLink`
removed from all HC templates; per-provider links render directly and human clicks
are tracked via Umami `campaign-click`. See runbook "Jun 22 2026 — @TrackLink on
per-subscriber CTAs."
3. **Reframed unsubstantiated per-record status assertions** to honest, hedged,
generally-true statements (defamation / FTC-deception risk).
4. This compliance review.
@ -84,8 +89,9 @@ These are factual compliance claims and must be **literally true**:
## HTML / deliverability QA — PASS
- All 10 templates render with **0 JS errors** headless, each has **exactly one
tracked `/order/...@TrackLink` CTA**, and **no price leaks** (only the $20,000
OIG penalty stat remains, intentionally).
per-provider `/order/...` CTA** (direct link, `@TrackLink` removed Jun 22 2026 —
see item 2), and **no price leaks** (only the $20,000 OIG penalty stat remains,
intentionally).
- External self-verify links (oig.hhs.gov, sam.gov, npiregistry, data.cms.gov) left
**untracked** on purpose (they're trust links, not conversions).

View file

@ -128,10 +128,20 @@ def _cta(label):
# so the template appends its own params with a leading `&` — correct whether
# or not the coupon is on. (Previously this used `?dot=`, which double-`?`d
# the URL once the coupon added its own query.)
#
# NO `@TrackLink` here: Listmonk registers a *single static URL per tracked
# link* and points every recipient's /link/<uuid> redirect at it. For a
# per-subscriber URL like lp_link that is doubly broken — (1) the registered
# URL was captured before the `{{ lp_link }}` token rendered, dropping the
# `?dot=` and producing `/order/slug&utm_source=...` (no `?`) which 404s, and
# (2) even when valid it collapses EVERY carrier onto the first subscriber's
# DOT. Real human clicks are tracked via Umami's `campaign-click` event
# (bot-filtered), so Listmonk link tracking is redundant here. Rendering the
# link directly gives each carrier their own correct `?dot=` URL, no redirect.
return (
'<div style="text-align:center;margin:24px 0">'
'<a href="{{ .Subscriber.Attribs.lp_link }}'
'&utm_source=listmonk&utm_medium=email&utm_campaign=deficiency@TrackLink" '
'&utm_source=listmonk&utm_medium=email&utm_campaign=deficiency" '
'style="display:inline-block;padding:14px 36px;background:#f97316;color:#fff;'
f'font-weight:700;border-radius:8px;text-decoration:none;font-size:16px">{label} &rarr;</a></div>'
)
@ -149,7 +159,7 @@ def _dot_check_cta():
'<p style="font-size:13px;color:#64748b;line-height:1.5;margin:0 0 10px">'
'Want to verify everything else on your DOT profile first?</p>'
f'<a href="{b.SITE_DOMAIN}/tools/dot-compliance-check?dot={{{{ .Subscriber.Attribs.dot_number }}}}'
'&utm_source=listmonk&utm_medium=email&utm_campaign=deficiency_dot_check@TrackLink" '
'&utm_source=listmonk&utm_medium=email&utm_campaign=deficiency_dot_check" '
'style="display:inline-block;padding:13px 30px;background:#f97316;color:#fff;'
'font-weight:700;border-radius:8px;text-decoration:none;font-size:15px">'
'Run Free DOT Compliance Check &rarr;</a></div>'

View file

@ -46,7 +46,13 @@ const mode = service_slug === "bdc-voice" ? "voice" : service_slug === "bdc-broa
</style>
<script>
const mode = (document.querySelector(".pw-step[data-mode]") as HTMLElement).getAttribute("data-mode")!;
// Guard: only run on pages that actually have the BDC data step. Without this
// the top-level querySelector returns null on every other order page and the
// `.getAttribute` throws, killing this script (a harmless-looking but real
// page error). The cast `as HTMLElement` masked the null at compile time.
const bdcRoot = document.querySelector(".pw-step[data-mode]") as HTMLElement | null;
if (bdcRoot) {
const mode = bdcRoot.getAttribute("data-mode")!;
const voice = document.getElementById("pw-voice-subs") as HTMLInputElement | null;
const snap = document.getElementById("pw-snapshot") as HTMLInputElement | null;
const file = document.getElementById("pw-bdc-file") as HTMLInputElement | null;
@ -86,4 +92,5 @@ const mode = service_slug === "bdc-voice" ? "voice" : service_slug === "bdc-broa
}
PW.patchIntakeData(patch);
});
} // end BDC step guard
</script>

View file

@ -214,8 +214,11 @@
return { primary, categories: [...new Set(cats)], reason };
}
// Render Q&A
const container = document.getElementById("cw-questions")!;
// Render Q&A. This script is bundled into every order page, so the container
// is absent on pages without the classification step — bail rather than throw
// on the top-level renderQuestion(0) call below (container.appendChild → null).
const container = document.getElementById("cw-questions");
if (container) {
const answers: Record<string, string> = {};
let questionIndex = 0;
@ -306,4 +309,5 @@
// Also start immediately if this is the current step
renderQuestion(0);
} // end classification step guard
</script>

View file

@ -465,6 +465,7 @@
if (dfwpRow) dfwpRow.hidden = false;
if (dfwpStateEl && d.state_dfwp) dfwpStateEl.value = d.state_dfwp;
}
});
// Save all data on step-next
window.addEventListener("pw:step-next", (evt) => {

View file

@ -83,7 +83,8 @@
return row;
}
document.getElementById("pw-add-circuit")!.addEventListener("click", () => {
const addCircuitBtn = document.getElementById("pw-add-circuit");
if (addCircuitBtn) addCircuitBtn.addEventListener("click", () => {
circuitsDiv.appendChild(newCircuitRow());
});

View file

@ -90,7 +90,13 @@
</style>
<script>
const grid = document.getElementById("pw-fq-grid")!;
// Guard: this step's script is bundled into every order page. Only initialize
// when the foreign-qualification grid is actually present, otherwise the many
// top-level getElementById(...)! lookups below return null and throw, killing
// the shared bundle (and any sibling step wired after it).
const fqGrid = document.getElementById("pw-fq-grid");
if (fqGrid) {
const grid = fqGrid as HTMLElement;
const countEl = document.getElementById("pw-fq-count")!;
const quoteDiv = document.getElementById("pw-fq-quote") as HTMLElement;
const quoteBody = document.getElementById("pw-fq-quote-body")!;
@ -238,4 +244,5 @@
});
loadJurisdictions();
} // end foreign_qual step guard
</script>

View file

@ -49,7 +49,15 @@ const vertical = slugVertical(service_slug);
</style>
<script>
const slug = document.querySelector(".pw-step[data-slug]")!.getAttribute("data-slug")!;
// The service slug MUST come from the wizard root, not the first
// `.pw-step[data-slug]`: on multi-step verticals (e.g. trucking) the DOT intake
// step renders before the review step and carries its own data-slug
// ("dot-intake"), so querySelector(".pw-step[data-slug]") returned the wrong
// value and the order-create POST sent service_slug="dot-intake" → HTTP 501,
// blocking every cold-visitor checkout on those pages.
const slug = document.querySelector(".pw-wizard[data-service]")?.getAttribute("data-service")
|| document.querySelector(".pw-step[data-slug]")?.getAttribute("data-slug")
|| "";
const entDiv = document.getElementById("pw-summary-entity")!;
const intakeDiv = document.getElementById("pw-summary-intake")!;
const errDiv = document.getElementById("pw-review-errors") as HTMLDivElement;

View file

@ -69,7 +69,8 @@
upstreamInput.style.display = showUpstream ? "" : "none";
}
g<HTMLSelectElement>("pw-ss-status").addEventListener("change", updateSSFields);
const ssStatusEl = g<HTMLSelectElement>("pw-ss-status");
if (ssStatusEl) ssStatusEl.addEventListener("change", updateSSFields);
window.addEventListener("pw:step-shown", (evt: any) => {
if (evt.detail.step !== "stir_shaken") return;