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
This commit is contained in:
justin 2026-06-02 03:51:25 -05:00
parent 426fbb2ea1
commit b85be726b7
5 changed files with 215 additions and 16 deletions

View file

@ -2066,15 +2066,11 @@ async function sendComplianceIntakeEmail(
</p>
</div>` : "";
// 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<string>([
// (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));

View file

@ -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<string, FieldSpec> = {
// 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<string, string[]> = {
"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<string> = 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;

View file

@ -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

View file

@ -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:

View file

@ -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<string, string> = {
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<string, string> = {
{steps.includes("ocn") && <div data-step="ocn" hidden><OCNStep /></div>}
{steps.includes("mcs150") && <div data-step="mcs150" hidden><MCS150Step /></div>}
{steps.includes("dot-intake") && <div data-step="dot-intake" hidden><DOTIntakeStep /></div>}
{steps.includes("state-trucking") && <div data-step="state-trucking" hidden><StateTruckingIntakeStep /></div>}
{steps.includes("classification") && <div data-step="classification" hidden><ClassificationWizard /></div>}
{steps.includes("review") && <div data-step="review" hidden><ReviewStep service_slug={service_slug} /></div>}
{steps.includes("payment") && <div data-step="payment" hidden><PaymentStep service_slug={service_slug} /></div>}
@ -226,7 +230,7 @@ const STEP_LABELS: Record<string, string> = {
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