Telegram notifications: - Add shared scripts/workers/telegram_notify.py (send_telegram, notify_fulfillment_todo, create_admin_todo) so every worker alerts the operator the same way; fire-and-forget. - Fire notify_fulfillment_todo after each admin_todos insert across all 8 service handlers (9 sites) so no fulfillment task waits unseen. (Orders + quotes + tickets already notified via checkout/quotes/tickets routes.) Client portal order progress: - order-timeline: derive real per-step status from live signals (payment paid, e-signature signed, fulfillment_status) instead of a static template; add current_step to the response. - Extract pure applyLiveStatus into order-timeline-status.ts (DB-free) + unit test (api/test/test_timeline_status.ts, 8 cases). - portal /me now returns compliance_orders.fulfillment_status. - Dashboard renders a client-safe Progress badge (In progress / Action needed / Filed-awaiting-confirmation / Completed); batches show the most actionable status. No back-office mechanics exposed. ERPNext sync parity: - Create a Sales Order for formation and fcc_carrier_registration orders (previously only canada_crtc + compliance synced); write erpnext_sales_order back to each table. Non-blocking, matches existing pattern. Verified: API tsc clean, timeline unit tests 8/8, Astro build 58 pages, cms10114/ink/paper_batch Python tests still green, no mechanics leaks.
131 lines
6.1 KiB
Python
131 lines
6.1 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
|
|
|
|
from scripts.workers.telegram_notify import notify_fulfillment_todo
|
|
|
|
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()
|
|
notify_fulfillment_todo(
|
|
title=title,
|
|
order_number=order_number,
|
|
service_slug=slug,
|
|
priority=priority,
|
|
description=description,
|
|
)
|
|
conn.close()
|
|
except Exception as exc:
|
|
LOG.error("[%s] Failed to create close-out todo: %s", order_number, exc)
|