From b85be726b775a8f1524f411dc3d2fc472f8c7aa3 Mon Sep 17 00:00:00 2001
From: justin
Date: Tue, 2 Jun 2026 03:51:25 -0500
Subject: [PATCH] feat(fulfillment): bundle/exclusion enforcement +
REQUIRED_FIELDS + intake wiring (Phases 1/1.5/2)
- compliance-orders: hazmat-phmsa/state-emissions products, full REQUIRED_FIELDS
table for all DOT/state/hazmat slugs, BUNDLE_COMPONENTS dedup + MUTUALLY_EXCLUSIVE
enforcement on /batch (single source of truth, exported)
- checkout: empty ADMIN_ASSISTED_SLUGS (state/hazmat now get intake links)
- services/__init__: register HazmatPHMSAHandler + state-emissions handler
- state_trucking: _summarize_intake admin-todo enrichment
- Wizard: wire StateTruckingIntakeStep + step labels
---
api/src/routes/checkout.ts | 14 +--
api/src/routes/compliance-orders.ts | 112 ++++++++++++++++++++-
scripts/workers/services/__init__.py | 6 ++
scripts/workers/services/state_trucking.py | 93 ++++++++++++++++-
site/src/components/intake/Wizard.astro | 6 +-
5 files changed, 215 insertions(+), 16 deletions(-)
diff --git a/api/src/routes/checkout.ts b/api/src/routes/checkout.ts
index 25d0c3a..574527e 100644
--- a/api/src/routes/checkout.ts
+++ b/api/src/routes/checkout.ts
@@ -2066,15 +2066,11 @@ async function sendComplianceIntakeEmail(
` : "";
- // Fully admin-assisted services — NO customer intake form (state-level filings
- // collected by ops). Everything else (FCC + federal DOT: MCS-150, BOC-3, UCR,
- // authority, D&A, audit, etc.) now has a customer intake page and gets a link.
- const ADMIN_ASSISTED_SLUGS = new Set([
- "irp-registration", "ifta-application", "ifta-quarterly",
- "or-weight-mile-tax", "ny-hut-registration", "ky-kyu-registration",
- "nm-weight-distance", "ct-highway-use-fee", "ca-mcp-carb",
- "state-dot-registration", "intrastate-authority", "osow-permit",
- "state-trucking-bundle",
+ // Fully admin-assisted services with NO customer intake form. State-level
+ // trucking + hazmat/emissions now have a dedicated intake step, so they are
+ // NO LONGER in this set — customers get an intake link like other services.
+ const ADMIN_ASSISTED_SLUGS = new Set([
+ // (reserved for any future no-intake services)
]);
const dotOrders = orders.filter(o => ADMIN_ASSISTED_SLUGS.has(o.service_slug as string));
const fccOrders = orders.filter(o => !ADMIN_ASSISTED_SLUGS.has(o.service_slug as string));
diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts
index 9691a89..dfb3316 100644
--- a/api/src/routes/compliance-orders.ts
+++ b/api/src/routes/compliance-orders.ts
@@ -373,6 +373,20 @@ const COMPLIANCE_SERVICES: Record<
erpnext_item: "STATE-TRUCKING-BUNDLE",
discountable: true,
},
+ // ── Hazmat / Emissions ───────────────────────────────────────────────
+ "hazmat-phmsa": {
+ name: "PHMSA Hazmat Registration",
+ price_cents: 14900, // $149 admin-assisted; PHMSA gov fee billed at cost
+ gov_fee_label: "PHMSA registration fee ($25 + $250-$3,000 processing, by business size, billed at cost)",
+ erpnext_item: "HAZMAT-PHMSA",
+ discountable: true,
+ },
+ "state-emissions": {
+ name: "State Clean-Truck / Emissions Compliance",
+ price_cents: 19900, // $199 — NY/CO/MD/NJ/MA clean-truck / ACT advisory + registration assist
+ erpnext_item: "STATE-EMISSIONS",
+ discountable: true,
+ },
// ── Corporate / Entity Services ──
"annual-report-filing": {
@@ -555,8 +569,78 @@ const REQUIRED_FIELDS: Record = {
// Foreign qualification — the target_states array is the critical input.
"foreign-qualification-single": { required: ["legal_name", "home_state_code", "entity_type", "target_states"], soft: ["ein"] },
"foreign-qualification-multi": { required: ["legal_name", "home_state_code", "entity_type", "target_states"], soft: ["ein"] },
+
+ // ── DOT / FMCSA Motor Carrier Services ───────────────────────────────
+ // All collected via the unified dot-intake step (DOTIntakeStep.astro).
+ "mcs150-update": { required: ["dot_number", "legal_name", "address_street", "address_city", "address_state", "address_zip", "phone", "email", "signer_name", "signer_title", "power_units", "drivers", "carrier_operation", "interstate_intrastate", "hazmat"], soft: ["mc_number", "ein", "annual_miles", "cargo_types"] },
+ "ucr-registration":{ required: ["dot_number", "legal_name", "address_state", "email", "fleet_size_bracket"], soft: ["mc_number", "power_units"] },
+ "boc3-filing": { required: ["dot_number", "legal_name", "address_street", "address_city", "address_state", "address_zip", "email"], soft: ["mc_number", "docket_type", "docket_number", "entity_type"] },
+ "dot-registration":{ required: ["legal_name", "address_street", "address_city", "address_state", "address_zip", "phone", "email", "signer_name", "signer_title", "carrier_operation", "interstate_intrastate", "hazmat"], soft: ["ein", "power_units", "drivers"] },
+ "mc-authority": { required: ["dot_number", "legal_name", "address_street", "address_city", "address_state", "address_zip", "phone", "email", "signer_name", "signer_title", "carrier_operation"], soft: ["mc_number", "ein"] },
+ "dot-drug-alcohol":{ required: ["dot_number", "legal_name", "email", "cdl_drivers"], soft: ["owner_operators", "der_name", "current_da_provider"] },
+ "dot-audit-prep": { required: ["dot_number", "legal_name", "email", "carrier_operation", "power_units", "drivers"], soft: ["cdl_drivers"] },
+ "dot-full-compliance": { required: ["dot_number", "legal_name", "address_street", "address_city", "address_state", "address_zip", "phone", "email", "signer_name", "signer_title", "carrier_operation", "interstate_intrastate", "hazmat", "power_units", "drivers"], soft: ["mc_number", "ein", "fleet_size_bracket", "cdl_drivers"] },
+ "usdot-reactivation": { required: ["dot_number", "legal_name", "address_street", "address_city", "address_state", "address_zip", "phone", "email", "signer_name", "signer_title"], soft: ["mc_number"] },
+ "emergency-temporary-authority": { required: ["dot_number", "legal_name", "email", "signer_name", "signer_title", "carrier_operation"], soft: ["mc_number"] },
+ "carrier-closeout": { required: ["dot_number", "legal_name", "email", "signer_name", "signer_title"], soft: ["mc_number"] },
+ "entity-dissolution": { required: ["legal_name", "address_state", "email", "signer_name"], soft: ["entity_type"] },
+
+ // ── State-Level Trucking Compliance ──────────────────────────────────
+ // Collected via the state-trucking intake step (StateTruckingIntakeStep.astro).
+ "irp-registration": { required: ["dot_number", "legal_name", "base_state", "email", "power_units", "operating_states"], soft: ["mc_number", "fuel_type", "gross_weight_bracket"] },
+ "ifta-application": { required: ["dot_number", "legal_name", "base_state", "email", "power_units", "fuel_type"], soft: ["mc_number", "operating_states"] },
+ "ifta-quarterly": { required: ["dot_number", "legal_name", "base_state", "email"], soft: ["reporting_quarter"] },
+ "or-weight-mile-tax": { required: ["dot_number", "legal_name", "email", "power_units"], soft: ["gross_weight_bracket"] },
+ "ny-hut-registration": { required: ["dot_number", "legal_name", "email", "power_units"], soft: ["gross_weight_bracket"] },
+ "ky-kyu-registration": { required: ["dot_number", "legal_name", "email", "power_units"], soft: [] },
+ "nm-weight-distance": { required: ["dot_number", "legal_name", "email", "power_units"], soft: [] },
+ "ct-highway-use-fee": { required: ["dot_number", "legal_name", "email", "power_units"], soft: ["gross_weight_bracket"] },
+ "ca-mcp-carb": { required: ["dot_number", "legal_name", "email", "power_units"], soft: ["ca_number", "engine_model_years"] },
+ "state-dot-registration":{ required: ["dot_number", "legal_name", "base_state", "email"], soft: [] },
+ "intrastate-authority": { required: ["dot_number", "legal_name", "base_state", "email", "authority_type"], soft: ["insurance_carrier", "insurance_policy", "boc3_on_file"] },
+ "osow-permit": { required: ["dot_number", "legal_name", "base_state", "email"], soft: ["load_dimensions", "load_weight"] },
+ "state-trucking-bundle": { required: ["dot_number", "legal_name", "base_state", "email", "power_units"], soft: ["operating_states", "fuel_type"] },
+
+ // ── Hazmat / Emissions ───────────────────────────────────────────────
+ "hazmat-phmsa": { required: ["dot_number", "legal_name", "email", "hazmat_classes"], soft: ["bulk_packaging", "small_business", "ein"] },
+ "state-emissions": { required: ["dot_number", "legal_name", "base_state", "email"], soft: ["engine_model_years", "power_units"] },
};
+// ── Bundle composition + incompatibility (single source of truth) ──────────
+// A bundle slug -> the individual component slugs it already covers. When a
+// bundle is in the cart, its components are dropped (no double-charge / dup
+// filing). Mirrors the DB service_bundles table for the DOT/FCC inline bundles.
+const BUNDLE_COMPONENTS: Record = {
+ "dot-full-compliance": [
+ "mcs150-update", "boc3-filing", "ucr-registration",
+ "dot-drug-alcohol", "dot-audit-prep",
+ ],
+ "state-trucking-bundle": [
+ "irp-registration", "ifta-application", "or-weight-mile-tax",
+ "ny-hut-registration", "ky-kyu-registration", "nm-weight-distance",
+ "ct-highway-use-fee", "ca-mcp-carb", "state-dot-registration",
+ "intrastate-authority",
+ ],
+ "new-carrier-bundle": [
+ "dot-registration", "mc-authority", "boc3-filing",
+ "mcs150-update", "dot-drug-alcohol", "ucr-registration",
+ ],
+ "fcc-full-compliance": [
+ "fcc-499a", "stir-shaken", "cpni-certification", "rmd-filing",
+ ],
+ "fcc-499a-499q": ["fcc-499a", "fcc-499q"],
+};
+
+// Mutually-exclusive services that must never be in the same cart.
+const MUTUALLY_EXCLUSIVE_GROUPS: string[][] = [
+ // A carrier is either reactivating OR closing out — not both.
+ ["usdot-reactivation", "carrier-closeout"],
+ // Emergency temporary authority vs full MC authority application.
+ ["emergency-temporary-authority", "mc-authority"],
+ // Standalone 499-A vs the A+Q bundle (also handled by BUNDLE_COMPONENTS).
+ ["fcc-499a", "fcc-499a-zero"],
+];
+
// Entity-level requirements (e.g. "must have an FRN on file before this
// service can run"). Checked against the linked telecom_entity.
const REQUIRES_ENTITY_FRN: ReadonlySet = new Set([
@@ -1151,9 +1235,29 @@ router.post("/api/v1/compliance-orders/batch", async (req, res) => {
// Deduplicate and validate service slugs
let services = [...new Set(rawServices as string[])];
- // If both 499a and 499a-499q selected, drop the standalone 499a
- if (services.includes("fcc-499a") && services.includes("fcc-499a-499q")) {
- services = services.filter(s => s !== "fcc-499a");
+ // ── Bundle / incompatibility enforcement (single source of truth) ──────
+ // If a bundle is selected, drop any of its individual components from the
+ // cart (the bundle already covers them) to avoid double-charging + duplicate
+ // filings. Then reject any hard-incompatible (mutually-exclusive) pairs.
+ const droppedComponents: string[] = [];
+ for (const [bundle, components] of Object.entries(BUNDLE_COMPONENTS)) {
+ if (services.includes(bundle)) {
+ const before = services.length;
+ services = services.filter(s => !components.includes(s));
+ if (services.length < before) {
+ droppedComponents.push(...components.filter(c => (rawServices as string[]).includes(c)));
+ }
+ }
+ }
+ for (const group of MUTUALLY_EXCLUSIVE_GROUPS) {
+ const present = group.filter(s => services.includes(s));
+ if (present.length > 1) {
+ res.status(400).json({
+ error: `These services cannot be ordered together: ${present.join(", ")}. Please choose one.`,
+ incompatible: present,
+ });
+ return;
+ }
}
const invalid = services.filter(s => !COMPLIANCE_SERVICES[s]);
@@ -2010,5 +2114,5 @@ router.post("/api/v1/compliance-orders/:id/usac-delegation", async (req, res) =>
}
});
-export { COMPLIANCE_SERVICES, REQUIRED_FIELDS };
+export { COMPLIANCE_SERVICES, REQUIRED_FIELDS, BUNDLE_COMPONENTS, MUTUALLY_EXCLUSIVE_GROUPS };
export default router;
diff --git a/scripts/workers/services/__init__.py b/scripts/workers/services/__init__.py
index 95d9168..634693f 100644
--- a/scripts/workers/services/__init__.py
+++ b/scripts/workers/services/__init__.py
@@ -55,6 +55,8 @@ from .ein_application import EINApplicationHandler
from .mailbox_setup import MailboxSetupHandler
# Carrier close-out / trucking wrap-up (shutdown) + entity dissolution
from .carrier_closeout import CarrierCloseoutHandler
+# PHMSA hazmat registration (admin-assisted, 49 CFR Part 107)
+from .hazmat_phmsa import HazmatPHMSAHandler
SERVICE_HANDLERS: dict[str, type] = {
"flsa-audit": FLSAAuditHandler,
@@ -134,6 +136,10 @@ SERVICE_HANDLERS: dict[str, type] = {
"intrastate-authority": StateTruckingHandler,
"osow-permit": StateTruckingHandler,
"state-trucking-bundle": StateTruckingHandler,
+ # ── Hazmat / PHMSA Registration ───────────────────────────────────
+ "hazmat-phmsa": HazmatPHMSAHandler,
+ # ── State emissions / clean-truck (admin-assisted) ────────────────
+ "state-emissions": StateTruckingHandler,
}
# Service slugs that operate on a telecom entity — used by job_server.py
diff --git a/scripts/workers/services/state_trucking.py b/scripts/workers/services/state_trucking.py
index 2e43a9c..49bae8d 100644
--- a/scripts/workers/services/state_trucking.py
+++ b/scripts/workers/services/state_trucking.py
@@ -184,6 +184,19 @@ SERVICE_INFO = {
"7. Send all registrations and confirmations to client",
],
},
+ "state-emissions": {
+ "name": "State Clean-Truck / Emissions Compliance",
+ "category": "emissions",
+ "steps": [
+ "1. Identify carrier's base/operating states with emissions programs "
+ "(NY, CO, MD, NJ, MA, etc. — Advanced Clean Trucks / Clean Truck Check)",
+ "2. Review fleet engine model-years against state emissions thresholds",
+ "3. Register/report fleet in the applicable state emissions portal",
+ "4. File any required compliance certification or fee",
+ "5. Set up annual renewal/reporting reminders",
+ "6. Send compliance confirmation + next-steps to client",
+ ],
+ },
}
@@ -220,6 +233,9 @@ class StateTruckingHandler:
base_state = intake.get("base_state", intake.get("phy_state", ""))
operating_states = intake.get("operating_states", [])
+ # Slug-specific intake fields collected by StateTruckingIntakeStep.
+ intake_summary = self._summarize_intake(service_slug, intake)
+
# Look up state requirements if we have a base state
state_reqs = None
if base_state:
@@ -244,10 +260,18 @@ class StateTruckingHandler:
"base_state": base_state,
"operating_states": operating_states,
"intake_data": intake,
+ "intake_summary": intake_summary,
"state_requirements": state_reqs,
"steps": steps,
}
+ # Render the slug-specific intake fields into the description.
+ intake_lines = ""
+ if intake_summary:
+ intake_lines = "\nFiling details:\n" + "\n".join(
+ f" - {k}: {v}" for k, v in intake_summary.items()
+ ) + "\n"
+
try:
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
@@ -269,8 +293,9 @@ class StateTruckingHandler:
f"DOT: {dot_number}\n"
f"Base state: {base_state}\n"
f"Operating states: {', '.join(operating_states) if operating_states else 'N/A'}\n"
- f"Customer: {customer_email}\n\n"
- f"Steps:\n" + "\n".join(steps),
+ f"Customer: {customer_email}\n"
+ + intake_lines +
+ f"\nSteps:\n" + "\n".join(steps),
json.dumps(todo_data),
))
conn.commit()
@@ -287,6 +312,70 @@ class StateTruckingHandler:
return []
+ # Map slug -> list of (intake_data key, human label) to surface in the todo.
+ _INTAKE_FIELD_MAP = {
+ "_common": [
+ ("power_units", "Power units"),
+ ("mc_number", "MC/MX/FF #"),
+ ],
+ "irp-ifta": [
+ ("fuel_type", "Fuel type"),
+ ("gross_weight_bracket", "Gross weight bracket"),
+ ],
+ "emissions": [
+ ("ca_number", "CA #"),
+ ("engine_model_years", "Oldest engine model year"),
+ ],
+ "intrastate": [
+ ("authority_type", "Authority type"),
+ ("boc3_on_file", "BOC-3 on file"),
+ ("insurance_carrier", "Insurance carrier"),
+ ("insurance_policy", "Insurance policy #"),
+ ],
+ "osow": [
+ ("load_dimensions", "Load dimensions"),
+ ("load_weight", "Load weight (lbs)"),
+ ],
+ "hazmat": [
+ ("hazmat_classes", "Hazmat classes"),
+ ("bulk_packaging", "Bulk packaging"),
+ ("small_business", "Small business"),
+ ],
+ }
+
+ # Which field groups apply to each slug (mirrors the front-end ST_SECTIONS).
+ _SLUG_FIELD_GROUPS = {
+ "irp-registration": ["irp-ifta"],
+ "ifta-application": ["irp-ifta"],
+ "ifta-quarterly": ["irp-ifta"],
+ "or-weight-mile-tax": ["irp-ifta"],
+ "ny-hut-registration": ["irp-ifta"],
+ "ky-kyu-registration": ["irp-ifta"],
+ "nm-weight-distance": ["irp-ifta"],
+ "ct-highway-use-fee": ["irp-ifta"],
+ "ca-mcp-carb": ["emissions"],
+ "state-emissions": ["emissions"],
+ "state-dot-registration": [],
+ "intrastate-authority": ["intrastate"],
+ "osow-permit": ["osow"],
+ "state-trucking-bundle": ["irp-ifta", "emissions", "intrastate"],
+ "hazmat-phmsa": ["hazmat"],
+ }
+
+ def _summarize_intake(self, service_slug: str, intake: dict) -> dict:
+ """Extract the slug-relevant intake fields into a flat label->value dict."""
+ groups = ["_common"] + self._SLUG_FIELD_GROUPS.get(service_slug, [])
+ summary: dict = {}
+ for group in groups:
+ for key, label in self._INTAKE_FIELD_MAP.get(group, []):
+ val = intake.get(key)
+ if val in (None, "", [], {}):
+ continue
+ if isinstance(val, (list, tuple)):
+ val = ", ".join(str(v) for v in val)
+ summary[label] = val
+ return summary
+
def _resolve_slug(self, order_number: str) -> str:
"""Look up the service_slug from compliance_orders by order_number."""
try:
diff --git a/site/src/components/intake/Wizard.astro b/site/src/components/intake/Wizard.astro
index b998311..1c68470 100644
--- a/site/src/components/intake/Wizard.astro
+++ b/site/src/components/intake/Wizard.astro
@@ -41,6 +41,7 @@ import CDRPeriodStep from "./steps/CDRPeriodStep.astro";
import OCNStep from "./steps/OCNStep.astro";
import MCS150Step from "./steps/MCS150Step.astro";
import DOTIntakeStep from "./steps/DOTIntakeStep.astro";
+import StateTruckingIntakeStep from "./steps/StateTruckingIntakeStep.astro";
import ClassificationWizard from "./steps/ClassificationWizard.astro";
import ReviewStep from "./steps/ReviewStep.astro";
import PaymentStep from "./steps/PaymentStep.astro";
@@ -78,6 +79,8 @@ const STEP_LABELS: Record = {
cpni_questions: "CPNI Details",
cdr_period: "Reporting Period",
ocn: "OCN Details",
+ "dot-intake": "Carrier Details",
+ "state-trucking": "Filing Details",
review: "Review",
payment: "Payment",
};
@@ -122,6 +125,7 @@ const STEP_LABELS: Record = {
{steps.includes("ocn") &&
}
{steps.includes("mcs150") &&
}
{steps.includes("dot-intake") &&
}
+ {steps.includes("state-trucking") &&
}
{steps.includes("classification") &&
}
{steps.includes("review") &&
}
{steps.includes("payment") && }
@@ -226,7 +230,7 @@ const STEP_LABELS: Record = {
block6_cert: "Certifications", bdc_data: "BDC Data",
stir_shaken: "STIR/SHAKEN", calea: "CALEA",
foreign_carrier: "Foreign Affiliation", foreign_qual: "State Registration", dc_agent: "D.C. Agent", cpni_questions: "CPNI Details", cdr_period: "Reporting Period",
- ocn: "OCN Details", review: "Review", payment: "Payment",
+ ocn: "OCN Details", "dot-intake": "Carrier Details", "state-trucking": "Filing Details", review: "Review", payment: "Payment",
};
// Category-gated dynamic step insertion. After the user picks Line 105