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:
parent
426fbb2ea1
commit
b85be726b7
5 changed files with 215 additions and 16 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue