diff --git a/api/src/routes/dot-lookup.ts b/api/src/routes/dot-lookup.ts
index 55a34db..0a8ff3a 100644
--- a/api/src/routes/dot-lookup.ts
+++ b/api/src/routes/dot-lookup.ts
@@ -973,8 +973,14 @@ router.get("/api/v1/dot/census", async (req, res) => {
phy_zip: c.phy_zip || null,
power_units: c.nbr_power_unit ?? null,
drivers: c.driver_total ?? null,
+ total_drivers: c.driver_total ?? null,
for_hire: c.authorized_for_hire || false,
carrier_operation: c.carrier_operation || null,
+ // Normalized interstate/intrastate code (A/B/C) if the stored value
+ // encodes it, for the intake prefill.
+ carrier_operation_code: (typeof c.carrier_operation === "string"
+ && /^[ABC]$/i.test(c.carrier_operation.trim()))
+ ? c.carrier_operation.trim().toUpperCase() : null,
});
} catch (err) {
console.error("[dot-census] Error:", err);
diff --git a/scripts/workers/services/mcs150_update.py b/scripts/workers/services/mcs150_update.py
index 4d2798f..e3937cd 100644
--- a/scripts/workers/services/mcs150_update.py
+++ b/scripts/workers/services/mcs150_update.py
@@ -124,6 +124,23 @@ class MCS150UpdateHandler:
LOG.info("[%s] Backfilled signer_name from signed certification: %s",
order_number, signed_name)
+ # INTAKE-COMPLETENESS GATE. The FMCSA census gives us the carrier's
+ # registered name/address/fleet, but it cannot tell us the operational
+ # details the MCS-150 requires the filer to confirm/update: the
+ # operation classification (Q23), cargo types (Q24), current annual
+ # mileage (Q21 -- must be CURRENT), and a contact email. If those are
+ # missing we must NOT fabricate a filing; instead we ask the customer to
+ # complete a short, census-pre-filled intake and hold the order until
+ # they do. (New-registration / reactivation flows that have not yet
+ # signed also route through here.)
+ missing = self._missing_intake_fields(slug, intake)
+ if missing and not client_approved and not admin_approved:
+ self._request_intake_completion(
+ order_number, entity_name, customer_email, dot_number, missing)
+ LOG.info("[%s] Held for customer intake completion; missing=%s",
+ order_number, missing)
+ return []
+
# Step 1: Fill the official MCS-150 PDF
pdf_path = None
try:
@@ -355,6 +372,117 @@ class MCS150UpdateHandler:
return [minio_path] if minio_path else []
+ def _missing_intake_fields(self, slug: str, intake: dict) -> list:
+ """Return the customer-required intake fields still missing for this
+ service. These are the operational details the FMCSA census cannot
+ supply and that the MCS-150 requires the filer to confirm/update.
+ """
+ # Per-variant required operational fields.
+ base_required = ["carrier_operation", "interstate_intrastate",
+ "annual_miles", "email"]
+ # Cargo only applies to carriers (not pure intermodal-equipment providers).
+ if not intake.get("is_intermodal_equipment_provider"):
+ base_required.append("cargo_types")
+ # New registrations / reactivations also need the signer + address.
+ if slug in ("dot-registration", "usdot-reactivation", "dot-full-compliance"):
+ base_required += ["signer_name", "signer_title", "address_street",
+ "address_city", "address_state", "address_zip"]
+ # Hazmat safety-permit (150B) needs states of operation.
+ if intake.get("hazmat") == "yes" and intake.get("needs_hmsp"):
+ base_required.append("operating_states")
+
+ missing = []
+ for f in base_required:
+ v = intake.get(f)
+ if v in (None, "", [], {}):
+ missing.append(f)
+ return missing
+
+ def _request_intake_completion(self, order_number, entity_name,
+ customer_email, dot_number, missing):
+ """Email the customer a census-pre-filled intake link and create a
+ low-priority admin todo noting we're waiting on intake completion."""
+ domain = os.environ.get("PUBLIC_SITE_URL", "https://performancewest.net").rstrip("/")
+ intake_url = f"{domain}/order/dot-compliance?order={order_number}"
+
+ # Customer email (no paper/mail mechanics; public form names are fine).
+ try:
+ from scripts.workers.worker_email import send_worker_email
+ label = {
+ "carrier_operation": "how your company operates (for-hire, private, etc.)",
+ "interstate_intrastate": "interstate vs intrastate operation",
+ "annual_miles": "your current annual mileage",
+ "cargo_types": "the types of cargo you haul",
+ "email": "a contact email address",
+ "operating_states": "the states you operate in",
+ "signer_name": "the name of the company officer signing",
+ "signer_title": "the officer's title",
+ }
+ needed = ", ".join(label.get(m, m.replace("_", " ")) for m in missing)
+ subject = f"Action needed: confirm your MCS-150 details (DOT {dot_number})"
+ text = (
+ f"Hi,\n\n"
+ f"We're ready to prepare the MCS-150 update for {entity_name} "
+ f"(USDOT {dot_number}). We've pre-filled everything we can from "
+ f"your current FMCSA record. To finish, we just need you to "
+ f"confirm a few current details: {needed}.\n\n"
+ f"Please review and confirm here (it takes about 2 minutes):\n"
+ f"{intake_url}\n\n"
+ f"Once you submit, we'll prepare your filing for your signature "
+ f"and handle the rest.\n\n"
+ f"Thank you,\nPerformance West"
+ )
+ html = (
+ f"
Hi,
"
+ f"
We're ready to prepare the MCS-150 update for "
+ f"{entity_name} (USDOT {dot_number}). We've "
+ f"pre-filled everything we can from your current FMCSA record. "
+ f"To finish, we just need you to confirm a few current details: "
+ f"{needed}.
Once you submit, we'll prepare your filing for your "
+ f"signature and handle the rest.
"
+ f"
Thank you, Performance West
"
+ )
+ if customer_email:
+ send_worker_email(customer_email, subject, html, text=text,
+ cc="justin@performancewest.net")
+ except Exception as exc:
+ LOG.warning("[%s] Could not send intake-completion email: %s",
+ order_number, exc)
+
+ # Hold the order + admin todo.
+ self._set_fulfillment_status(order_number, "awaiting_intake")
+ try:
+ import psycopg2
+ conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
+ with conn.cursor() as cur:
+ cur.execute(
+ """INSERT INTO admin_todos (
+ title, category, priority, order_number, service_slug,
+ description, data, status
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, 'pending')""",
+ (
+ f"Awaiting MCS-150 intake -- {entity_name} (DOT {dot_number})",
+ "intake", "normal", order_number, self.SERVICE_SLUG,
+ (f"Order {order_number} is missing customer-required intake "
+ f"fields: {', '.join(missing)}.\n"
+ f"A census-pre-filled intake link was emailed to "
+ f"{customer_email or 'the customer'}.\n"
+ f"Intake link: {intake_url}\n"
+ f"Filing auto-resumes once the customer submits."),
+ json.dumps({"order_number": order_number, "dot_number": dot_number,
+ "entity_name": entity_name, "awaiting_intake": True,
+ "missing_fields": missing}),
+ ),
+ )
+ conn.commit()
+ conn.close()
+ except Exception as exc:
+ LOG.warning("[%s] Could not create intake-pending todo: %s",
+ order_number, exc)
+
def _restamp_signed_form(self, order_number: str, document_key: str) -> None:
"""Point the signed esign record at ``document_key`` (the freshly filled
form) and re-stamp the signature onto it, so the signed PDF an admin
diff --git a/site/src/components/intake/Wizard.astro b/site/src/components/intake/Wizard.astro
index 557ddc8..ca59275 100644
--- a/site/src/components/intake/Wizard.astro
+++ b/site/src/components/intake/Wizard.astro
@@ -420,6 +420,40 @@ const STEP_LABELS: Record = {
state.entity = { ...state.entity, frn: intake.frn };
}
}
+
+ // For DOT/MCS-150 orders, seed any still-empty fields from the
+ // carrier's current FMCSA census so the customer only has to fill
+ // the operational details FMCSA cannot give us (operation class,
+ // cargo, current mileage, email). Customer-entered values always win.
+ const dotForCensus = (state.intake_data.dot_number as string) || "";
+ const slug = (order.service_slug as string) || "";
+ const isDotOrder = /^(mcs150|usdot|dot-|mc-)/.test(slug) || /mcs150|usdot-reactivation|dot-registration|dot-full/.test(slug);
+ if (dotForCensus && isDotOrder) {
+ try {
+ const cr = await fetch(`${API}/api/v1/dot/census?dot=${dotForCensus.replace(/\D/g, "")}`);
+ if (cr.ok) {
+ const c = await cr.json();
+ const d = state.intake_data;
+ const seed = (k: string, v: any) => { if (v && !d[k]) d[k] = v; };
+ seed("legal_name", c.legal_name);
+ seed("dba_name", c.dba_name);
+ seed("address_street", c.phy_street);
+ seed("address_city", c.phy_city);
+ seed("address_state", c.phy_state);
+ seed("address_zip", c.phy_zip);
+ seed("phone", c.telephone);
+ seed("power_units", c.power_units != null ? String(c.power_units) : "");
+ seed("drivers", c.total_drivers != null ? String(c.total_drivers) : "");
+ // Interstate/intrastate from the census operation code.
+ if (!d.interstate_intrastate && c.carrier_operation_code) {
+ const code = String(c.carrier_operation_code).toUpperCase();
+ d.interstate_intrastate = code === "A" ? "interstate"
+ : code === "B" ? "intrastate_hazmat"
+ : code === "C" ? "intrastate_non_hazmat" : d.interstate_intrastate;
+ }
+ }
+ } catch { /* census seed is best-effort */ }
+ }
saveState(state);
}
} catch {}
diff --git a/site/src/components/intake/steps/MCS150Step.astro b/site/src/components/intake/steps/MCS150Step.astro
index cf32b45..ec09ab0 100644
--- a/site/src/components/intake/steps/MCS150Step.astro
+++ b/site/src/components/intake/steps/MCS150Step.astro
@@ -83,11 +83,91 @@
-
+
+
+
+
+
+
Mailing Address
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Filing Details
+
+
+
+
Entity & Operations
@@ -158,6 +238,25 @@
+
+
+
+
+
Cargo Types (check all that apply)
@@ -192,6 +291,73 @@
+
+
Hazmat Safety Permit (HM-232)
+
These fields only apply to carriers transporting hazardous materials that require a Hazardous Materials Safety Permit (HMSP). If that does not apply to you, leave this section blank.