# Plan ## Status: COMPLETE (all phases implemented + validated 2026-06-02) | Phase | What | Status | |-------|------|--------| | 1 | Hazmat/PHMSA handler + product (`hazmat-phmsa`, $149) | ✅ | | 1.5 | Order-form bundle/mutual-exclusion enforcement (server-side) | ✅ | | 2 | State-trucking intake form (slug-gated) + REQUIRED_FIELDS + admin-todo fields | ✅ | | 2.5 | BOC-3 authority-aware (active/pending/revoked/none) + upsell-approve follow-ups | ✅ | | 2.6 | Pipeline activation gating (`require_active` edges, FMCSA poll, waiting_on_activation) | ✅ | | 3 | State emissions (non-CA) product `state-emissions` ($199) | ✅ | | 4 | Order landing pages for all state/hazmat/emissions slugs (48 pages build) | ✅ | | Adv | Prerequisite-aware DOT lookup + state recommendations | ✅ | | 5 | Campaign builder deficiency segments + LP routing + --list-segments | ✅ | | Val | Consistency checker (24/24) + campaign segment test (synthetic) | ✅ | **Remaining ops (not code):** create the 6 Listmonk source-campaign templates and set their `CAMPAIGN_*_ID` envs (`CAMPAIGN_FOR_HIRE_ID`, `CAMPAIGN_IRP_IFTA_ID`, `CAMPAIGN_INTRASTATE_ID`, `CAMPAIGN_WEIGHT_TAX_ID`, `CAMPAIGN_EMISSIONS_ID`, `CAMPAIGN_HAZMAT_ID`). Until set, those segments are reported by `--list-segments` but skipped by the scheduled run. Optional follow-up: a client-side incompatibility UX hint in the order form (server already enforces). ## Goal Make every trucker deficiency type we flag actually *fulfillable*: each flagged deficiency must have (1) a service handler that can complete the work, (2) a checkout/order path, and (3) an intake form that collects **all** information that handler needs before the job runs. Only after fulfillment is complete and verified do we extend the campaign builder to email those deficiency segments. ## Scope / affected areas Deficiency flags in play (live counts): for_hire (19,811), interstate_irp_ifta (19,761), intrastate_authority (14,081), state_emissions (12,424), state_weight_tax (6,289), state_permit (3,418), mcs150_overdue (4,539), hazmat (514), zero_fleet (134). Files / systems: - `scripts/workers/services/__init__.py` — handler registry - `scripts/workers/services/state_trucking.py` — IRP/IFTA/weight-tax/permit/intrastate handler (admin-todo only today) - `scripts/workers/services/mcs150_update.py` — MCS-150 + reactivation (real FMCSA filing) - `scripts/workers/services/boc3_filing.py` — BOC-3 (Playwright) - **NEW** `scripts/workers/services/hazmat_phmsa.py` — only fully-missing fulfillment path - `site/src/lib/intake_manifest.ts` — per-service intake steps + pricing/meta - `site/src/components/intake/steps/DOTIntakeStep.astro` — unified DOT intake (no state-trucking sections today) - **NEW** `site/src/components/intake/steps/StateTruckingIntakeStep.astro` — state filing fields - `api/src/routes/compliance-orders.ts` — `COMPLIANCE_SERVICES` (products) + `REQUIRED_FIELDS` (validation; **none defined for any DOT/state-trucking slug today**) - `api/src/routes/checkout.ts` — slug allowlist - `api/src/routes/dot-lookup.ts` — recommended-services mapping - **NEW** `site/src/pages/order/*.astro` — landing pages for state-trucking + hazmat (none exist) - `scripts/build_trucking_campaigns.py` — campaign builder (extend last) ## Key findings (grounding) 1. **Fulfillment handlers already exist** for ~98% of flags. The single fully missing path is **hazmat / PHMSA registration** (no handler, product, page). 2. **State-trucking intake collects nothing.** All 13 state slugs map to `["review"]` in `intake_manifest.ts` with a comment "info collected at checkout" — but checkout collects no per-filing fields. So IRP has no vehicle/weight/jurisdiction data, IFTA has no fleet/base-state, NY HUT has no vehicle list, intrastate-authority has no insurance/authority-type, etc. The handler's admin todo is therefore incomplete and an admin must chase the customer for data. 3. **`REQUIRED_FIELDS` has zero entries** for any DOT or state-trucking slug, so the API performs no intake validation for these orders. 4. **No dedicated order landing pages** for IRP/IFTA/state-tax/permit/intrastate or hazmat. Checkout works by slug, but campaign emails have no clean LP to drive conversions. 5. **State emissions** flags (NY/CO/MD/NJ/MA/etc., 12,424) only map to a product for CA (CARB via `ca-mcp-carb`). Non-CA emissions have no product — decide whether to build or fold into existing state DOT/permit service. ## Approach (concrete ordered steps) ### Phase 1 — Close the hazmat fulfillment gap (only fully-missing path) 1. Add `HazmatPHMSAHandler` in `scripts/workers/services/hazmat_phmsa.py` (admin-assisted, mirrors `state_trucking.py`: creates admin_todo with PHMSA registration steps, sends status email). Slug `hazmat-phmsa`. 2. Register `"hazmat-phmsa": HazmatPHMSAHandler` in `services/__init__.py`. 3. Add product to `COMPLIANCE_SERVICES`, meta to `intake_manifest.ts`, slug to checkout allowlist. ### Phase 1.5 — Order-form incompatibility enforcement (bundles vs components, dupes) Today the batch endpoint (`compliance-orders.ts` POST `/batch`) only dedupes and hard-codes one case (drop standalone `fcc-499a` when `fcc-499a-499q` present). There is no general rule preventing a customer from selecting a **bundle plus its own components** (e.g. `dot-full-compliance` + `mcs150-update` + `boc3-filing`, or `state-trucking-bundle` + `irp-registration`), or other incompatible combos. This double-charges and creates duplicate filings. Build a single source of truth for service relationships and enforce it: - Add `BUNDLE_COMPONENTS` map (bundle slug -> component slugs) covering `dot-full-compliance`, `state-trucking-bundle`, `new-carrier-bundle`, `fcc-full-compliance`, plus the DB `service_bundles` rows. - Add `INCOMPATIBLE_PAIRS` / mutually-exclusive groups (e.g. `usdot-reactivation` vs `carrier-closeout`; `fcc-499a` vs `fcc-499a-499q`; emergency-temp-authority vs mc-authority where applicable). - Server-side (authoritative) in `/batch`: when a bundle is present, **drop any of its components** from the cart (keep the bundle), reject hard-incompatible pairs with a clear error, and keep dedup. Generalize the existing 499a special-case into this map. - Client-side (order form / cart UI): disable/grey out a component when its parent bundle is selected (and vice-versa: selecting all components suggests the bundle), and prevent selecting mutually-exclusive options, with an inline explanation. Mirror the server map so UX matches enforcement. ### Phase 2 — Make state-trucking intake actually collect required data 4. Build `StateTruckingIntakeStep.astro` (one shared step, sections shown by slug, mirroring `DOTIntakeStep.astro`'s section-gating pattern): - Carrier identity: legal name, DOT#, MC#, base state, contact (prefill from DOT lookup). - IRP/IFTA: power units w/ VIN+plate+gross-weight rows, operating jurisdictions, fuel type. - Weight-distance (OR/NY/KY/NM/CT): vehicle list + gross weights + (OR) declared combined weight. - CA MCP+CARB: fleet engine model-years for CARB Clean Truck Check, CA# if any. - Intrastate authority: authority type, insurance carrier + policy#, cargo, BOC-3 on file? - State DOT / OSOW: as needed. 5. Update `intake_manifest.ts`: replace `["review"]` with `["state-trucking", "review"]` for the 13 slugs; wire the step into the Wizard. 6. Add `REQUIRED_FIELDS` entries in `compliance-orders.ts` for each state-trucking slug + the DOT slugs (mcs150, ucr, boc3, dot-registration, mc-authority, etc.) so intake is validated server-side. Mirror handler "Intake data needed" docstrings. 7. Update `state_trucking.py` handler to read + surface the new intake fields in the admin todo (so admins get vehicle lists, jurisdictions, insurance, etc.). ### Phase 2.5 — Make BOC-3 authority-aware (preexisting authority handling) The BOC-3 attaches to a carrier's operating authority (MC/FF/MX docket). Today `boc3_filing.py` only reads `commonAuthorityStatus`/`contractAuthorityStatus`/ `brokerAuthorityStatus` to print a status string and otherwise always files a fresh BOC-3. That can be wrong/wasteful depending on the preexisting authority. Add branching off the live FMCSA authority state: - **Active authority:** file/refresh BOC-3 only. (current behavior) - **Pending authority:** file BOC-3 + flag that active insurance must be on file for the authority to activate; create follow-up todo. - **Revoked/inactive authority:** file BOC-3 **and** flag/upsell reinstatement (OP-1 reinstatement + $80 gov fee, route via `usdot-reactivation`/`mc-authority` reinstatement branch). BOC-3 alone does not reinstate. - **No authority (USDOT only):** BOC-3 has nothing to attach to — flag that MC authority (`mc-authority`) is likely needed first; do not silently file. Implementation: have `process()/handle()` read full authority status (reuse `_check_boc3_status`, expanded to return structured fields), select the branch, adjust the admin-todo + customer email, and emit a `recommended_followups` list the order timeline / upsell can surface. No automatic charge for the follow-up — surface it for the customer/admin to approve. ### Phase 2.6 — Prerequisite/activation gating (wait for FMCSA "active", not just "submitted") There are real FMCSA dependency chains where a downstream filing must wait for an upstream item to be **active at the agency**, not merely submitted by us. The existing `pipeline_orchestrator.py` models ordering via `wait_for`, but a step is treated as satisfied when `pipeline_step_status == "completed"` (= our handler finished), which is NOT the same as FMCSA showing it active. True prerequisites to enforce: - **MC Authority (OP-1)** requires an **active USDOT**. - **Authority activation** requires **BOC-3 on file + insurance (BMC-91) on file**, then a **mandatory ~21-day vetting/protest period** before it goes active. BOC-3 + insurance CAN be filed while authority is pending (parallel OK). - **IRP / IFTA / intrastate-authority / UCR** that depend on *active* authority (or active USDOT) must wait for that activation, not just for the prior order to be submitted. Implementation: - Add an "activation gate" to the orchestrator: for dependency edges flagged `require_active: true`, poll FMCSA (mobile QC API for USDOT status; L&I for authority/BOC-3/insurance) and only mark the dependency satisfied when the agency reports active. Until then, hold the downstream step in a `waiting_on_activation` state with a next-poll timestamp. - Encode the 21-day authority vetting window as an expected-activation estimate so the timeline/customer comms set correct expectations. - Expand `PIPELINES` edges with `require_active` flags (USDOT→MC, USDOT→IRP/IFTA/ UCR/intrastate, authority-active→IRP-for-hire/intrastate). - Standalone (non-bundle) orders: when a single service is ordered whose prerequisite isn't active yet, surface a clear "blocked until X is active" status + recommended prerequisite order rather than filing prematurely. ### Phase 3 — Decide + handle state emissions (non-CA) 8. Either: (a) add a generic `state-emissions` service handler+product covering NY/CO/MD/NJ/MA clean-truck/ACT programs, or (b) map those flags to `state-dot-registration` / advisory-only. (Open question — see below.) ### Phase 4 — Order landing pages 9. Create `site/src/pages/order/*.astro` for: irp-ifta (combined), state weight taxes (one templated page per state or a single state-aware page), ca-mcp-carb, intrastate-authority, hazmat-phmsa. Reuse existing order-page layout (e.g. `boc3-filing.astro`, `ucr-registration.astro` as templates). ### Phase 5 — Extend campaign builder (only after 1-4 verified) 10. Add new campaign segments to `build_trucking_campaigns.py` keyed off `deficiency_flags`: for-hire/BOC-3+UCR, IRP/IFTA, intrastate-authority, state weight-tax (per-state), CA MCP/CARB, hazmat. Each links to its LP. 11. Create the corresponding Listmonk source campaigns (templates) and wire IDs. ## Validation (how each part is verified) - **Handlers:** unit-invoke each handler with a synthetic `order_data` dict in a throwaway script; assert an `admin_todos` row is created with the expected fields and (dev mode) no live filing fires. Confirm `SERVICE_HANDLERS` resolves every new slug. - **Intake completeness (the core ask):** write a check that, for every slug, cross-references `REQUIRED_FIELDS[slug]` against the fields the intake step emits and against the keys the handler reads — fail if a handler-needed field is never collected. This is the verifiable "we collect all needed info" metric. - **Checkout/products:** assert every flagged slug exists in `COMPLIANCE_SERVICES`, `SERVICE_META`, checkout allowlist, and `SERVICE_HANDLERS` (one consistency test). - **Order pages:** build site (`astro build` / existing build script) and confirm each new `/order/` route renders; smoke-load locally. - **Campaign builder:** run `build_trucking_campaigns.py --dry-run` and assert each new segment selects a nonzero, deduped audience pointing at a valid LP. - **End-to-end:** place a test order per new slug through checkout in dev, verify intake validation blocks missing fields, and the handler produces a complete admin todo. ## Open questions / decisions **RESOLVED (per user 2026-06-02):** 1. **BOC-3 follow-ups + prerequisite blockers → upsell-approve, advise pre-order where possible.** Two layers: - *Pre-order advisory (preferred):* extend the existing DOT Compliance Check tool (`site/public/tools/dot-compliance-check/`) + `dot-lookup` recommended services to be **prerequisite-aware**. When a recommended service needs an active prerequisite (e.g. IRP needs active authority), show the dependency, pre-select the prerequisite, order the cart correctly, and state the ~21-day authority activation expectation — all before payment. - *Post-order upsell-approve:* when a handler discovers a blocker mid-fulfillment (e.g. BOC-3 ordered but authority revoked → needs reinstatement), write a `recommended_followups` entry on the order and render a one-click, pre-filled "Add this service" card on the order timeline/portal. Customer confirms + pays via the existing checkout. **No auto-charge.** - *Standalone checkout guard:* if a service is bought directly and its prereq isn't active at FMCSA, warn + offer to add the prereq rather than filing something unfulfillable. 2. **State emissions (non-CA): build a real product.** Add a `state-emissions` service (handler + product + intake + page) covering NY/CO/MD/NJ/MA clean-truck / Advanced Clean Trucks programs (CA stays on `ca-mcp-carb`). 3. **Pre-order advisory: yes.** Covered by 1 above — reuse the compliance-check tool as the advisory surface. **STILL OPEN:** 4. Pricing for `hazmat-phmsa` and `state-emissions`. **DEFAULT:** `hazmat-phmsa` = $149 (admin-assisted PHMSA registration; gov fee $25 placardable-hazmat reg billed at cost). `state-emissions` = $199 (NY/CO/MD/NJ/MA clean-truck / ACT advisory + registration assist). 5. Vehicle-list intake fidelity. **DEFAULT:** lightweight up front — collect fleet count + base/operating states + fuel type + gross-weight bracket at intake; collect full per-vehicle VIN/plate/weight rows via a post-order follow-up form only when the specific filing requires it (IRP, weight-distance taxes). Keeps conversion high. 6. Standalone-order prereq guard. **DEFAULT:** warn + offer to add the prerequisite (pre-selected), allow override ("file anyway"). Never a hard block. 7. Long-term home for the reviewable plan (`docs/plans/` used here; no `side_panel` tool in this harness).