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.
This commit is contained in:
justin 2026-06-23 13:08:41 -05:00
parent 3325259af7
commit 5546c58bf0
7 changed files with 36 additions and 7 deletions

View file

@ -46,7 +46,13 @@ const mode = service_slug === "bdc-voice" ? "voice" : service_slug === "bdc-broa
</style> </style>
<script> <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 voice = document.getElementById("pw-voice-subs") as HTMLInputElement | null;
const snap = document.getElementById("pw-snapshot") as HTMLInputElement | null; const snap = document.getElementById("pw-snapshot") as HTMLInputElement | null;
const file = document.getElementById("pw-bdc-file") 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); PW.patchIntakeData(patch);
}); });
} // end BDC step guard
</script> </script>

View file

@ -214,8 +214,11 @@
return { primary, categories: [...new Set(cats)], reason }; return { primary, categories: [...new Set(cats)], reason };
} }
// Render Q&A // Render Q&A. This script is bundled into every order page, so the container
const container = document.getElementById("cw-questions")!; // 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> = {}; const answers: Record<string, string> = {};
let questionIndex = 0; let questionIndex = 0;
@ -306,4 +309,5 @@
// Also start immediately if this is the current step // Also start immediately if this is the current step
renderQuestion(0); renderQuestion(0);
} // end classification step guard
</script> </script>

View file

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

View file

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

View file

@ -90,7 +90,13 @@
</style> </style>
<script> <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 countEl = document.getElementById("pw-fq-count")!;
const quoteDiv = document.getElementById("pw-fq-quote") as HTMLElement; const quoteDiv = document.getElementById("pw-fq-quote") as HTMLElement;
const quoteBody = document.getElementById("pw-fq-quote-body")!; const quoteBody = document.getElementById("pw-fq-quote-body")!;
@ -238,4 +244,5 @@
}); });
loadJurisdictions(); loadJurisdictions();
} // end foreign_qual step guard
</script> </script>

View file

@ -49,7 +49,15 @@ const vertical = slugVertical(service_slug);
</style> </style>
<script> <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 entDiv = document.getElementById("pw-summary-entity")!;
const intakeDiv = document.getElementById("pw-summary-intake")!; const intakeDiv = document.getElementById("pw-summary-intake")!;
const errDiv = document.getElementById("pw-review-errors") as HTMLDivElement; const errDiv = document.getElementById("pw-review-errors") as HTMLDivElement;

View file

@ -69,7 +69,8 @@
upstreamInput.style.display = showUpstream ? "" : "none"; 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) => { window.addEventListener("pw:step-shown", (evt: any) => {
if (evt.detail.step !== "stir_shaken") return; if (evt.detail.step !== "stir_shaken") return;