diff --git a/api/migrations/082_tickets_lead_categories.sql b/api/migrations/082_tickets_lead_categories.sql index d5b2c97..274c8b6 100644 --- a/api/migrations/082_tickets_lead_categories.sql +++ b/api/migrations/082_tickets_lead_categories.sql @@ -1,7 +1,7 @@ --- Widen tickets.category to allow lead-capture categories posted by the DOT --- compliance checker (insurance, business close-out, truck-sale routing). --- The CHECK constraint previously only allowed the support-widget categories, --- so these INSERTs failed with a 500. +-- Widen tickets.category to allow the lead-capture categories posted by the DOT +-- compliance checker (insurance quote + truck-sale routing). The CHECK constraint +-- previously only allowed the support-widget categories, so these INSERTs 500'd. +-- (Business close-out is handled as a paid order, not a ticket, so it's not here.) ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_category_check; @@ -13,8 +13,8 @@ ALTER TABLE tickets ADD CONSTRAINT tickets_category_check CHECK ( 'service_request', 'quote', 'insurance_lead', - 'business_closeout', 'truck_sale_quickcash', - 'truck_sale_marketplace' + 'truck_sale_marketplace', + 'truck_sale_both' ]::text[]) ); diff --git a/api/src/routes/compliance-orders.ts b/api/src/routes/compliance-orders.ts index e4e3d42..aa2fb9c 100644 --- a/api/src/routes/compliance-orders.ts +++ b/api/src/routes/compliance-orders.ts @@ -281,6 +281,18 @@ const COMPLIANCE_SERVICES: Record< erpnext_item: "DOT-FULL-COMPLIANCE", discountable: true, }, + "carrier-closeout": { + name: "Trucking Wrap-Up (USDOT Shutdown)", + price_cents: 19900, // $199 — final MCS-150 (out of business) + MC revoke + UCR cancel + state account closures + erpnext_item: "CARRIER-CLOSEOUT", + discountable: true, + }, + "entity-dissolution": { + name: "Business Entity Dissolution (LLC/Corp)", + price_cents: 19900, // $199 add-on — LLC/Corp dissolution + final report; state filing fees billed separately + erpnext_item: "ENTITY-DISSOLUTION", + discountable: true, + }, // ── State-Level Trucking Compliance ────────────────────────────────── "irp-registration": { name: "IRP Registration Assistance", diff --git a/api/src/routes/tickets.ts b/api/src/routes/tickets.ts index 1156919..b7ad24b 100644 --- a/api/src/routes/tickets.ts +++ b/api/src/routes/tickets.ts @@ -7,7 +7,7 @@ const router = Router(); const VALID_CATEGORIES = [ "question", "support", "issue", "service_request", "quote", - "insurance_lead", "business_closeout", "truck_sale_quickcash", "truck_sale_marketplace", + "insurance_lead", "truck_sale_quickcash", "truck_sale_marketplace", "truck_sale_both", ] as const; // POST /api/v1/tickets diff --git a/scripts/workers/services/__init__.py b/scripts/workers/services/__init__.py index e28d759..95d9168 100644 --- a/scripts/workers/services/__init__.py +++ b/scripts/workers/services/__init__.py @@ -53,6 +53,8 @@ from .state_trucking import StateTruckingHandler # EIN application + virtual mailbox from .ein_application import EINApplicationHandler from .mailbox_setup import MailboxSetupHandler +# Carrier close-out / trucking wrap-up (shutdown) + entity dissolution +from .carrier_closeout import CarrierCloseoutHandler SERVICE_HANDLERS: dict[str, type] = { "flsa-audit": FLSAAuditHandler, @@ -112,6 +114,8 @@ SERVICE_HANDLERS: dict[str, type] = { "emergency-temporary-authority": MCS150UpdateHandler, # ask.fmcsa.dot.gov type 308 "ein-application": EINApplicationHandler, "virtual-mailbox": MailboxSetupHandler, + "carrier-closeout": CarrierCloseoutHandler, # trucking wrap-up / USDOT shutdown + "entity-dissolution": CarrierCloseoutHandler, # add-on, same handler (dissolution branch) "annual-report-filing": MCS150UpdateHandler, # admin-assisted "registered-agent": MCS150UpdateHandler, # admin-assisted (NWRA/CorpTools) "entity-reinstatement": MCS150UpdateHandler, # admin-assisted diff --git a/scripts/workers/services/carrier_closeout.py b/scripts/workers/services/carrier_closeout.py new file mode 100644 index 0000000..975f745 --- /dev/null +++ b/scripts/workers/services/carrier_closeout.py @@ -0,0 +1,89 @@ +"""Carrier Close-Out — Trucking Wrap-Up workflow. + +Done-for-you shutdown of a motor carrier. Orchestrates the sequential +wind-down as an admin-tracked workflow: + final MCS-150 (Out of Business) -> revoke MC authority -> cancel UCR -> + close IFTA/IRP + state accounts -> advise on insurance timing. + +The `entity-dissolution` add-on (separate slug, same handler) dissolves the +LLC/Corp and files the final report — gated on a no-outstanding-liabilities +attestation. + +Intake data: + - entity_name / legal_name, dot_number, mc_number + - phy_state / state, operating_states +""" +from __future__ import annotations + +import json +import logging +import os + +LOG = logging.getLogger("workers.services.carrier_closeout") + + +class CarrierCloseoutHandler: + SERVICE_SLUG = "carrier-closeout" + + async def process(self, order_data: dict) -> list[str]: + order_number = order_data.get("order_number", order_data.get("name", "")) + return self.handle(order_data, order_number) + + def handle(self, order_data: dict, order_number: str) -> list[str]: + intake = order_data.get("intake_data") or {} + if isinstance(intake, str): + intake = json.loads(intake) + + slug = order_data.get("service_slug", self.SERVICE_SLUG) + name = intake.get("entity_name") or intake.get("legal_name") or "Unknown carrier" + dot = intake.get("dot_number") or intake.get("usdot") or "N/A" + state = intake.get("phy_state") or intake.get("state") or "N/A" + LOG.info("[%s] Carrier close-out (%s) for %s (DOT %s)", order_number, slug, name, dot) + + if slug == "entity-dissolution": + steps = [ + "Confirm NO outstanding lawsuits, liens, or judgments before dissolving (client attestation).", + f"File Articles of Dissolution for {name} with the {state} Secretary of State.", + "File final state report / franchise tax return; close state tax accounts.", + "Notify IRS (final return, check the 'final' box); close the EIN if requested.", + ] + title = f"Entity Dissolution — {name} ({state})" + else: + steps = [ + f"File final MCS-150 marking carrier OUT OF BUSINESS — deactivate USDOT {dot}.", + "Submit voluntary revocation of operating authority (MC) to FMCSA.", + "Cancel UCR registration; confirm marked inactive (no next-year bill).", + "Close IFTA account, file final quarterly return, retire decals.", + f"Return IRP apportioned plates to base state ({state}); close the account.", + "Close state-level accounts/permits (CA MCP/CARB, OR weight-mile, NY HUT, KY KYU, etc.) per operating states.", + "Advise client on insurance cancellation timing (only AFTER authority is revoked).", + ] + title = f"Trucking Wrap-Up (Shutdown) — {name} (DOT {dot})" + + description = ( + f"Carrier: {name}\nUSDOT: {dot}\nMC: {intake.get('mc_number', 'N/A')}\n" + f"Base state: {state}\nOperating states: {intake.get('operating_states', 'N/A')}\n\n" + "Sequential wind-down steps:\n" + + "\n".join(f" {i + 1}. {s}" for i, s in enumerate(steps)) + ) + self._create_todo(order_number, intake, title, description, slug, priority="high") + return [f"Close-out workflow queued: {title}"] + + def _create_todo(self, order_number, intake, title, description, slug, priority="normal"): + 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') + """, + (title, "filing", priority, order_number, slug, description, json.dumps(intake)), + ) + conn.commit() + conn.close() + except Exception as exc: + LOG.error("[%s] Failed to create close-out todo: %s", order_number, exc) diff --git a/site/public/tools/dot-compliance-check/index.html b/site/public/tools/dot-compliance-check/index.html index 985749f..cb7ca43 100644 --- a/site/public/tools/dot-compliance-check/index.html +++ b/site/public/tools/dot-compliance-check/index.html @@ -286,64 +286,59 @@ Send reset link function buildCloseoutHtml(data) { var state = data.phy_state || ""; - var steps = []; - steps.push("File a final MCS-150 marking your carrier “Out of Business.” This deactivates USDOT " + data.dot_number + " so biennial-update obligations and $1,000/day late penalties stop accruing."); - steps.push("Voluntarily revoke your operating authority (MC) with FMCSA so it isn’t left active and billable after you stop running."); - steps.push("Cancel your UCR registration — don’t just skip renewal; confirm you’re marked inactive so you aren’t billed next year."); - steps.push("Close your IFTA account, file a final quarterly return, and return or destroy your IFTA decals."); - steps.push("Return your IRP apportioned plates" + (state ? " to your base state (" + state + ")" : "") + " and close the account so registration fees stop."); - if (STATE_CLOSEOUT[state]) steps.push("State requirement: " + STATE_CLOSEOUT[state]); - steps.push("Cancel your insurance — but only after your authority is revoked, so FMCSA doesn’t flag an insurance lapse on an active carrier."); - steps.push("Dissolve your LLC / corporation with the state and file a final tax return so you stop owing annual-report fees and franchise tax."); + var orderUrl = "/order/dot-compliance?dot=" + data.dot_number + "&services=carrier-closeout&intent=closing"; + + var includes = []; + includes.push("File your final MCS-150 (Out of Business) to deactivate USDOT " + data.dot_number + " — stops biennial-update obligations and $1,000/day late penalties."); + includes.push("Revoke your operating authority (MC) with FMCSA so it isn’t left active and billable."); + includes.push("Cancel your UCR registration and confirm you’re marked inactive so next year’s bill never comes."); + includes.push("Close your IFTA account, file the final quarterly return, and handle your decals."); + includes.push("Return your IRP plates" + (state ? " to your base state (" + state + ")" : "") + " and close the account."); + if (STATE_CLOSEOUT[state]) includes.push("Close your state account: " + STATE_CLOSEOUT[state]); + includes.push("We tell you exactly when to cancel your insurance so FMCSA doesn’t flag a lapse."); var h = ''; - // Intro - h += '
Closing a trucking business is more than parking the truck. Skip a step and the bills — UCR, IRP, state permits, annual reports — keep coming. Here’s what to wrap up, based on your FMCSA record:
'; - h += 'Closing ' + data.legal_name + '? Don’t leave loose ends that keep billing you after you’re off the road. We do the whole shutdown for you — no Login.gov, no government portals:
'; + h += 'We’ll deactivate your USDOT, revoke your authority, cancel UCR, and close your state accounts — so nothing keeps billing you after you’re out. No Login.gov, no government portals.
'; - h += ''; - h += ''; - h += ''; - h += ''; - h += ''; + h += ''; + h += 'Also closing the business entity? Add LLC/Corp dissolution + final report at checkout. Don’t dissolve if you have any outstanding lawsuits, liens, or judgments — settle those first; dissolving with open liabilities can put your personal assets at risk.
'; + h += 'Two ways to go — pick what fits your timeline:
'; + h += 'Pick what fits your timeline — or do both and take the better deal:
'; h += '