- Migration 079: state_trucking_requirements table seeded for all 51 jurisdictions (IRP, IFTA, weight-distance taxes, MCP/CARB, intrastate authority, state DOT) - Migration 080: carrier_operating_states tracking table - 13 new state trucking services in catalog ($99-$599) - StateTruckingHandler with state-specific admin todos - DOT compliance checker: 7 new state-level checks (IRP, IFTA, weight tax, MCP/CARB, emissions, intrastate authority, state DOT number) - New API endpoint: GET /api/v1/dot/state-requirements - DOT order page: state compliance service cards with auto-preselect - California trucking landing page (MCP + CARB + IRP + IFTA) - Fix: DOT checker nav missing Trucking/DOT section - Fix: All 8 DOT intake pages missing style block (dangling text) - Fix: DOT confirmation email now says "Order Confirmed" not "Action Required" - Fix: MCS150/BOC3/StateTrucking handlers missing async process() method - Fix: StateTruckingHandler connection leak + slug resolution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
203 lines
7.9 KiB
Python
203 lines
7.9 KiB
Python
"""
|
|
MCS-150 Biennial Update Service Handler.
|
|
|
|
The MCS-150 is filed through the FMCSA Portal (portal.fmcsa.dot.gov) using
|
|
Login.gov credentials + MFA. Since we can't automate through MFA, this is
|
|
an admin-assisted service:
|
|
|
|
1. Client orders MCS-150 update
|
|
2. We collect their updated info via intake form
|
|
3. Admin logs into FMCSA Portal with client's credentials (provided by client)
|
|
OR we prepare the data and walk the client through filing via screen share
|
|
4. We verify the update was accepted
|
|
5. We send confirmation with updated company snapshot
|
|
|
|
Service slug: mcs150-update
|
|
Price: $79
|
|
Gov fee: $0
|
|
|
|
Intake data needed:
|
|
- DOT number
|
|
- Legal name (confirm/update)
|
|
- DBA name (confirm/update)
|
|
- Principal business address
|
|
- Mailing address
|
|
- Phone number
|
|
- Email address
|
|
- Number of power units
|
|
- Number of drivers
|
|
- Operation type (interstate/intrastate)
|
|
- Carrier operation (authorized for hire, exempt for hire, private)
|
|
- Cargo types
|
|
- Hazmat (Y/N)
|
|
- Annual mileage + year
|
|
- FMCSA Portal login credentials (Login.gov email + password)
|
|
OR "I need help creating my Login.gov account"
|
|
|
|
Filing approach:
|
|
Option A: Client provides Login.gov credentials → admin files directly
|
|
Option B: Guided filing via screen share (Zoom/Teams) → $29 upcharge
|
|
Option C: We prepare a pre-filled PDF → client uploads themselves (cheapest)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from datetime import datetime
|
|
|
|
LOG = logging.getLogger("workers.services.mcs150_update")
|
|
|
|
|
|
class MCS150UpdateHandler:
|
|
"""Handle MCS-150 biennial update orders."""
|
|
|
|
SERVICE_SLUG = "mcs150-update"
|
|
SERVICE_NAME = "MCS-150 Biennial Update"
|
|
|
|
async def process(self, order_data: dict) -> list[str]:
|
|
"""Entry point called by job_server. Delegates to handle()."""
|
|
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]:
|
|
"""
|
|
Process an MCS-150 update order.
|
|
|
|
Since FMCSA Portal requires Login.gov MFA, this creates an admin
|
|
todo rather than automating the filing directly.
|
|
"""
|
|
LOG.info("[%s] Processing MCS-150 update order", order_number)
|
|
|
|
intake = order_data.get("intake_data") or {}
|
|
if isinstance(intake, str):
|
|
intake = json.loads(intake)
|
|
|
|
dot_number = intake.get("dot_number", "")
|
|
entity_name = intake.get("entity_name", order_data.get("customer_name", ""))
|
|
customer_email = order_data.get("customer_email", "")
|
|
|
|
# Validate required fields
|
|
if not dot_number:
|
|
LOG.error("[%s] Missing DOT number in intake data", order_number)
|
|
return []
|
|
|
|
# Check current MCS-150 status via FMCSA API
|
|
mcs150_status = self._check_current_status(dot_number)
|
|
|
|
# Create admin todo with all the info needed to file
|
|
todo_data = {
|
|
"order_number": order_number,
|
|
"service": self.SERVICE_NAME,
|
|
"dot_number": dot_number,
|
|
"entity_name": entity_name,
|
|
"customer_email": customer_email,
|
|
"current_status": mcs150_status,
|
|
"intake_data": intake,
|
|
"filing_url": "https://portal.fmcsa.dot.gov/login",
|
|
"steps": [
|
|
"1. Log into FMCSA Portal with client's Login.gov credentials",
|
|
"2. Navigate to Registration > MCS-150",
|
|
"3. Update fields with intake data provided",
|
|
"4. Verify all information is correct",
|
|
"5. Submit the update",
|
|
"6. Take screenshot of confirmation",
|
|
"7. Download updated company snapshot from SAFER",
|
|
"8. Email confirmation + snapshot to client",
|
|
],
|
|
}
|
|
|
|
# Create admin todo
|
|
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')
|
|
""", (
|
|
f"MCS-150 Update — {entity_name} (DOT {dot_number})",
|
|
"filing",
|
|
"normal",
|
|
order_number,
|
|
self.SERVICE_SLUG,
|
|
f"File MCS-150 biennial update for {entity_name}.\n"
|
|
f"DOT: {dot_number}\n"
|
|
f"Customer: {customer_email}\n"
|
|
f"Current MCS-150 status: {mcs150_status}\n\n"
|
|
f"Client intake data attached. Log into FMCSA Portal and update.",
|
|
json.dumps(todo_data),
|
|
))
|
|
conn.commit()
|
|
conn.close()
|
|
LOG.info("[%s] Admin todo created for MCS-150 update", order_number)
|
|
except Exception as exc:
|
|
LOG.error("[%s] Failed to create admin todo: %s", order_number, exc)
|
|
|
|
# Send client a status email
|
|
self._send_status_email(order_number, entity_name, dot_number, customer_email)
|
|
|
|
return [] # No generated files — admin handles the filing
|
|
|
|
def _check_current_status(self, dot_number: str) -> str:
|
|
"""Check current MCS-150 status via FMCSA API."""
|
|
try:
|
|
import urllib.request
|
|
api_key = os.environ.get("FMCSA_API_KEY", "")
|
|
if not api_key:
|
|
return "API key not configured"
|
|
|
|
url = f"https://mobile.fmcsa.dot.gov/qc/services/carriers/{dot_number}?webKey={api_key}"
|
|
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
data = json.loads(resp.read())
|
|
|
|
carrier = data.get("content", {}).get("carrier", {})
|
|
outdated = carrier.get("mcs150Outdated", "?")
|
|
status = carrier.get("statusCode", "?")
|
|
allowed = carrier.get("allowedToOperate", "?")
|
|
|
|
return (
|
|
f"Status: {status}, Allowed: {allowed}, "
|
|
f"MCS-150 Outdated: {outdated}"
|
|
)
|
|
except Exception as exc:
|
|
return f"Could not check: {exc}"
|
|
|
|
def _send_status_email(self, order_number, entity_name, dot_number, customer_email):
|
|
"""Send client an email that we're working on their update."""
|
|
if not customer_email:
|
|
return
|
|
try:
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
|
|
body = (
|
|
f"Hi,\n\n"
|
|
f"We've received your MCS-150 biennial update order for "
|
|
f"{entity_name} (DOT# {dot_number}).\n\n"
|
|
f"Order: {order_number}\n\n"
|
|
f"Our team will review your intake information and complete "
|
|
f"the filing within 1-2 business days. We'll send you a "
|
|
f"confirmation with your updated company snapshot once it's done.\n\n"
|
|
f"If we need your FMCSA Portal login credentials, we'll reach "
|
|
f"out via a separate secure email.\n\n"
|
|
f"Questions? Reply to this email or call (888) 411-0383.\n\n"
|
|
f"Performance West Inc.\n"
|
|
f"DOT Compliance Services\n"
|
|
)
|
|
|
|
msg = MIMEText(body)
|
|
msg["Subject"] = f"MCS-150 Update In Progress — {entity_name} (DOT {dot_number})"
|
|
msg["From"] = "noreply@performancewest.net"
|
|
msg["To"] = customer_email
|
|
|
|
with smtplib.SMTP("localhost", 25) as s:
|
|
s.sendmail(msg["From"], [customer_email], msg.as_string())
|
|
|
|
LOG.info("[%s] Status email sent to %s", order_number, customer_email)
|
|
except Exception as exc:
|
|
LOG.warning("[%s] Failed to send status email: %s", order_number, exc)
|