From d0d39ebcbc5acbdea5df7499f078536f6e382c0d Mon Sep 17 00:00:00 2001 From: justin Date: Tue, 2 Jun 2026 13:03:14 -0500 Subject: [PATCH] intake: validate cold ?dot= orders before checkout (fix 422) The cold-visitor Finish flow created the order then called Stripe Checkout directly, which is gated on intake_data_validated=true and returned 422 INTAKE_NOT_VALIDATED. Now call POST /compliance-orders/:n/validate between create and checkout (matching the token-order flow), and surface any missing required fields to the user instead of a raw HTTP error. --- site/src/components/intake/Wizard.astro | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/site/src/components/intake/Wizard.astro b/site/src/components/intake/Wizard.astro index 5245a6e..6730e5f 100644 --- a/site/src/components/intake/Wizard.astro +++ b/site/src/components/intake/Wizard.astro @@ -602,6 +602,25 @@ const STEP_LABELS: Record = { const order = await createResp.json(); const newOrderNumber = order.order_number; if (!newOrderNumber) throw new Error("Order created but no order number returned."); + // Validate the intake before checkout. Stripe Checkout is gated on + // intake_data_validated=true (set by this endpoint); without it the + // checkout call returns 422 INTAKE_NOT_VALIDATED. + const valResp = await fetch(`${API}/api/v1/compliance-orders/${newOrderNumber}/validate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }); + if (valResp.status === 422 || valResp.ok) { + const val = await valResp.json().catch(() => ({})); + if (val && val.ok === false) { + const miss = (val.missing || []).join(", "); + throw new Error( + "Please complete required fields" + (miss ? `: ${miss}` : "") + ".", + ); + } + } else if (!valResp.ok) { + throw new Error(`Validation HTTP ${valResp.status}`); + } // Kick off Stripe Checkout for the new order. const checkoutResp = await fetch(`${API}/api/v1/checkout/create-session`, { method: "POST",