From b25d1f5fd36e73bb9c418a90f2735276f6e9764c Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 31 May 2026 01:01:02 -0500 Subject: [PATCH] trucking wrap-up: close-out becomes a paid order + workflow - Checker closing mode now pitches a done-for-you 'Trucking Wrap-Up' ($199) with a buy button to /order/dot-compliance?services=carrier-closeout, instead of a lead form. DIY checklist replaced by what's-included list. - Entity dissolution offered as a paid add-on with the lawsuits/liens/judgments warning before dissolving. - New catalog services: carrier-closeout ($199), entity-dissolution ($199). - CarrierCloseoutHandler orchestrates the sequential shutdown workflow (final MCS-150 out-of-business, MC revoke, UCR cancel, IFTA/IRP + state closures; dissolution branch for the add-on) as admin-tracked tasks. - Sell-your-trucks: single shared form with quick-cash / marketplace / both; name field is now a real first+last name (no corp-name prefill). - tickets categories: add truck_sale_both, drop business_closeout (now an order). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../082_tickets_lead_categories.sql | 12 +- api/src/routes/compliance-orders.ts | 12 ++ api/src/routes/tickets.ts | 2 +- scripts/workers/services/__init__.py | 4 + scripts/workers/services/carrier_closeout.py | 89 +++++++++ .../tools/dot-compliance-check/index.html | 188 +++++++++--------- 6 files changed, 202 insertions(+), 105 deletions(-) create mode 100644 scripts/workers/services/carrier_closeout.py 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 += '
'; - h += '

Winding down ' + data.legal_name + '? Here’s your exit checklist.

'; - 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 += '
    '; - steps.forEach(function(s) { - h += '
  1. '; - h += ''; + // Done-for-you trucking wrap-up service ($199) + h += '
    '; + h += '

    Let us handle your entire trucking wrap-up

    '; + 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 += '
      '; + includes.forEach(function(s) { + h += '
    • '; + h += ''; h += '' + s + '
    • '; }); - h += '
'; - - // "Let us handle it" lead capture (green) - h += '
'; - h += '

Let us handle the shutdown paperwork

'; - 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 += '
'; + h += 'Start My Wrap-Up — $199 →'; + h += 'Flat $199. We file everything for you.'; + h += '
'; + // Entity dissolution add-on + liability caveat + 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 += '
'; h += '
'; - // Sell-your-trucks box (green) + // Sell-your-trucks box (green) — quick cash / marketplace / both, one shared form + var optStyle = "flex:1;min-width:180px;padding:14px;border-radius:10px;border:2px solid #86efac;background:#fff;color:#166534;font-weight:700;font-size:14px;cursor:pointer;text-align:left;line-height:1.4"; + var subStyle = "font-weight:400;font-size:12px;color:#15803d"; + var inStyle = "width:100%;padding:9px 12px;font-size:13px;border:1px solid #bbf7d0;border-radius:8px;outline:none;margin-bottom:8px;box-sizing:border-box"; h += '
'; h += '

Looking to sell your trucks?

'; - 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 += '
'; - h += ''; - h += ''; + h += ''; + h += ''; + h += ''; h += '
'; - // Quick-cash form (hidden until chosen) - h += ''; - // Marketplace form (hidden until chosen) - h += ''; @@ -386,67 +381,64 @@ Send reset link var loc = (data.phy_city || "") + ", " + (data.phy_state || ""); var fleet = data.fleet ? (data.fleet.power_units + " trucks, " + data.fleet.drivers + " drivers") : "unknown"; - // "Handle my shutdown" lead - var coBtn = document.getElementById("co-submit"); - if (coBtn) coBtn.addEventListener("click", function() { - var name = document.getElementById("co-name").value.trim(); - var email = document.getElementById("co-email").value.trim(); - var phone = document.getElementById("co-phone").value.trim(); - pwSubmitLead({ - category: "business_closeout", requireEmail: true, email: email, name: name, - btn: coBtn, statusEl: document.getElementById("co-status"), - okMsg: "Got it! We'll reach out within 1 business day to start your wind-down.", - subject: "Business Close-Out — " + data.legal_name + " (DOT " + data.dot_number + ")", - message: ["Business close-out / wind-down request from DOT Compliance Checker.", "", - "Carrier: " + data.legal_name, "DOT#: " + data.dot_number, "Location: " + loc, "Fleet: " + fleet, - "", "Contact: " + name, "Email: " + email, "Phone: " + (phone || data.telephone || "not provided")].join("\n") - }); - }); + var MODE = { + quickcash: { + intro: "We’ll connect you with a vetted truck-buying partner for a no-obligation cash offer. Tell us what you’re selling:", + truck: true, btn: "Get My Cash Offer →", category: "truck_sale_quickcash", + ok: "Got it — a truck buyer will reach out with a cash offer.", subj: "Truck Sale (Quick Cash)", + }, + marketplace: { + intro: "Smart move — a private/marketplace sale usually nets more than a quick buyout. We’ll email you our guide to the best truck marketplaces and how to list for top dollar.", + truck: false, btn: "Email Me the Marketplace Guide →", category: "truck_sale_marketplace", + ok: "Done! Watch your inbox for our truck-marketplace guide.", subj: "Truck Sale (Marketplace guide)", + }, + both: { + intro: "Best of both — we’ll line up a cash offer AND send you the marketplace guide so you can compare and take the better deal. Tell us what you’re selling:", + truck: true, btn: "Get a Cash Offer + the Guide →", category: "truck_sale_both", + ok: "Done! We’ll line up a cash offer and send the marketplace guide so you can compare.", subj: "Truck Sale (Both — cash offer + marketplace)", + }, + }; - // Sell-trucks option reveal + var current = null; + var form = document.getElementById("sell-form"); document.querySelectorAll(".sell-opt").forEach(function(b) { b.addEventListener("click", function() { - var qc = document.getElementById("sell-quickcash"); - var mp = document.getElementById("sell-marketplace"); - qc.style.display = b.dataset.opt === "quickcash" ? "block" : "none"; - mp.style.display = b.dataset.opt === "marketplace" ? "block" : "none"; + current = b.dataset.opt; + var m = MODE[current]; + if (form) form.style.display = "block"; + document.getElementById("sell-intro").innerHTML = m.intro; + document.getElementById("sell-truck").style.display = m.truck ? "block" : "none"; + document.getElementById("sell-submit").innerHTML = m.btn; + document.getElementById("sell-status").classList.add("hidden"); document.querySelectorAll(".sell-opt").forEach(function(x) { x.style.background = x === b ? "#dcfce7" : "#fff"; }); }); }); - // Quick-cash → truck-buyer referral lead - var qcBtn = document.getElementById("qc-submit"); - if (qcBtn) qcBtn.addEventListener("click", function() { - var name = document.getElementById("qc-name").value.trim(); - var phone = document.getElementById("qc-phone").value.trim(); - var email = document.getElementById("qc-email").value.trim(); - var truck = document.getElementById("qc-truck").value.trim(); + var sBtn = document.getElementById("sell-submit"); + if (sBtn) sBtn.addEventListener("click", function() { + if (!current) return; + var m = MODE[current]; + var name = document.getElementById("sell-name").value.trim(); + var phone = document.getElementById("sell-phone").value.trim(); + var email = document.getElementById("sell-email").value.trim(); + var truck = document.getElementById("sell-truck").value.trim(); + var statusEl = document.getElementById("sell-status"); + if (!name) { + statusEl.textContent = "Please enter your first & last name."; + statusEl.className = "text-xs text-red-600"; statusEl.classList.remove("hidden"); + return; + } pwSubmitLead({ - category: "truck_sale_quickcash", requireEmail: false, email: email, name: name, - btn: qcBtn, statusEl: document.getElementById("qc-status"), - okMsg: "Got it — we'll connect you with a truck buyer who'll reach out with a cash offer.", - subject: "Truck Sale (Quick Cash) — " + data.legal_name + " (DOT " + data.dot_number + ")", - message: ["Quick-cash truck sale lead from DOT Compliance Checker — route to truck-buying partner.", "", + category: m.category, requireEmail: true, email: email, name: name, + btn: sBtn, statusEl: statusEl, okMsg: m.ok, + subject: m.subj + " — " + data.legal_name + " (DOT " + data.dot_number + ")", + message: [m.subj + " lead from DOT Compliance Checker.", "", "Carrier: " + data.legal_name, "DOT#: " + data.dot_number, "Location: " + loc, "Fleet: " + fleet, - "Vehicle(s): " + (truck || "not specified"), "", - "Contact: " + name, "Email: " + email, "Phone: " + (phone || data.telephone || "not provided")].join("\n") - }); - }); - - // Marketplace → guide follow-up lead - var mpBtn = document.getElementById("mp-submit"); - if (mpBtn) mpBtn.addEventListener("click", function() { - var email = document.getElementById("mp-email").value.trim(); - pwSubmitLead({ - category: "truck_sale_marketplace", requireEmail: true, email: email, name: data.legal_name, - btn: mpBtn, statusEl: document.getElementById("mp-status"), - okMsg: "Done! Watch your inbox for our truck-marketplace guide.", - subject: "Truck Sale (Marketplace guide) — " + data.legal_name + " (DOT " + data.dot_number + ")", - message: ["Marketplace truck-sale lead from DOT Compliance Checker — send marketplace guide follow-up.", "", - "Carrier: " + data.legal_name, "DOT#: " + data.dot_number, "Location: " + loc, "Fleet: " + fleet, - "", "Email: " + email].join("\n") + (m.truck ? "Vehicle(s): " + (truck || "not specified") : null), "", + "Contact: " + name, "Email: " + email, "Phone: " + (phone || data.telephone || "not provided")] + .filter(function(x) { return x !== null; }).join("\n"), }); }); }