new-site/scripts/workers/services/carrier_closeout.py
justin 02112facf5 capture client signature before filing signed DOT forms
Forms that legally require the client's signature were not being captured
correctly:

- MCS-150 handler created a perjury e-sign record but then submitted to FMCSA
  anyway, before the client signed. Now it gates submission: request the
  signature, hold, and only file when handle_esign_completed re-dispatches with
  client_approved=True.
- MCS-150 e-sign links were signed with JWT_SECRET/ADMIN_JWT_SECRET, but the
  portal verifies with CUSTOMER_JWT_SECRET, so every link returned "Invalid
  portal link." New shared dot_esign helper signs with CUSTOMER_JWT_SECRET.
- carrier-closeout (final MCS-150 Out of Business) and entity-dissolution
  (Articles of Dissolution + no-lawsuits/liens/judgments attestation) captured
  no signature at all. Both now request a signed attestation before the
  workflow proceeds.
- mc-authority / emergency-temporary-authority now get a correctly labeled
  OP-1 applicant certification instead of an "MCS-150" record.

Also fixes a latent dispatcher bug: order["service_slug"] was never set, so
handlers sharing a class fell back to their default SERVICE_SLUG. This made
entity-dissolution run the carrier-closeout branch and mc-authority/etc. look
like mcs150-update. Now the resolved slug is injected into order_data.

Portal e-sign page now renders the document-specific certification text from
metadata.perjury_text (so the dissolution no-liabilities attestation and OP-1
cert are actually shown to the signer), not just a generic perjury line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:30:09 -05:00

122 lines
5.8 KiB
Python

"""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))
)
# Signature gate. Both close-out (final MCS-150 "Out of Business") and
# entity dissolution require the client's signed authorization/attestation
# before we file anything. On first run we request the signature and hold;
# handle_esign_completed re-dispatches with client_approved=True once signed.
from scripts.workers.services.dot_esign import requires_signature, request_dot_esign
client_approved = bool(order_data.get("client_approved"))
customer_email = order_data.get("customer_email", "")
if requires_signature(slug) and not client_approved:
request_dot_esign(
order_number=order_number,
slug=slug,
entity_name=name,
customer_email=customer_email,
dot_number=dot if dot != "N/A" else "",
)
LOG.info("[%s] Awaiting client signature before %s — holding workflow", order_number, slug)
self._create_todo(
order_number, intake,
f"{title} [AWAITING CLIENT SIGNATURE]",
description + f"\n\nStatus: AWAITING CLIENT SIGNATURE — link emailed to {customer_email}."
"\nWorkflow auto-resumes once the client signs.",
slug, priority="low",
)
return [f"Awaiting signature: {title}"]
# Signed (or re-dispatched after signing) — queue the actionable workflow.
self._create_todo(
order_number, intake,
f"{title} [SIGNED — READY TO FILE]",
description + "\n\nStatus: Client signed the authorization. Proceed with filing.",
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)