From 8441c6f0c06b055f478290d23b751c87f4dd9164 Mon Sep 17 00:00:00 2001 From: justin Date: Thu, 28 May 2026 22:37:47 -0500 Subject: [PATCH] Add MCS-150 and BOC-3 service handlers for trucking compliance MCS-150 Biennial Update ($79): - Admin-assisted filing (FMCSA Portal requires Login.gov MFA) - Creates admin todo with intake data and filing steps - Checks current MCS-150 status via FMCSA API - Sends status email to client BOC-3 Process Agent Filing ($149): - Partners with blanket process agent (NWRA or similar) - Collects carrier info, submits designation to partner - Partner files electronically with FMCSA - Stub for future process agent API integration - Sends status/confirmation emails Both follow the same handler pattern as FCC services (admin todo with structured data when full automation isn't possible). Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/workers/services/boc3_filing.py | 264 ++++++++++++++++++++++ scripts/workers/services/mcs150_update.py | 198 ++++++++++++++++ 2 files changed, 462 insertions(+) create mode 100644 scripts/workers/services/boc3_filing.py create mode 100644 scripts/workers/services/mcs150_update.py diff --git a/scripts/workers/services/boc3_filing.py b/scripts/workers/services/boc3_filing.py new file mode 100644 index 0000000..0c01b50 --- /dev/null +++ b/scripts/workers/services/boc3_filing.py @@ -0,0 +1,264 @@ +""" +BOC-3 Process Agent Designation Service Handler. + +The BOC-3 designates a process agent in every US state who can accept +legal documents on behalf of a motor carrier. The process agent (not +the carrier) files the form with FMCSA. + +Model: Performance West partners with a blanket process agent service +(e.g., NWRA or similar) who covers all 48 contiguous states + DC. +We collect the carrier's info, submit the designation to our process +agent partner, they file the BOC-3 electronically with FMCSA. + +Service slug: boc3-filing +Price: $149 +Gov fee: $0 + +Intake data needed: + - DOT number + - MC/FF/MX number (docket number) + - Legal name + - DBA name (if any) + - Business address + - Phone number + - Email + - Entity type (carrier, broker, freight forwarder) + +Filing flow: + 1. Client orders BOC-3 filing + 2. We collect intake data + 3. We submit designation request to process agent partner + 4. Process agent files BOC-3 electronically with FMCSA + 5. We verify filing on FMCSA L&I system + 6. We send confirmation to client +""" + +from __future__ import annotations + +import json +import logging +import os +from datetime import datetime + +LOG = logging.getLogger("workers.services.boc3_filing") + +# Process agent partner details — update when partnership is established +PROCESS_AGENT_PARTNER = { + "name": "TBD — NWRA or similar", + "contact_email": "", + "api_endpoint": None, # Will be set when partner API is available +} + + +class BOC3FilingHandler: + """Handle BOC-3 process agent designation orders.""" + + SERVICE_SLUG = "boc3-filing" + SERVICE_NAME = "BOC-3 Process Agent Filing" + + def handle(self, order_data: dict, order_number: str) -> list[str]: + """ + Process a BOC-3 filing order. + + Currently creates an admin todo. When process agent partner API + is available, this will automate the submission. + """ + LOG.info("[%s] Processing BOC-3 filing order", order_number) + + intake = order_data.get("intake_data") or {} + if isinstance(intake, str): + intake = json.loads(intake) + + dot_number = intake.get("dot_number", "") + docket_number = intake.get("docket_number", "") # MC-XXXXXX + entity_name = intake.get("entity_name", order_data.get("customer_name", "")) + customer_email = order_data.get("customer_email", "") + entity_type = intake.get("entity_type", "carrier") # carrier, broker, freight_forwarder + + if not dot_number: + LOG.error("[%s] Missing DOT number", order_number) + return [] + + # Check current BOC-3 status + boc3_status = self._check_boc3_status(dot_number) + + # Build the designation request + designation = { + "dot_number": dot_number, + "docket_number": docket_number, + "legal_name": entity_name, + "dba_name": intake.get("dba_name", ""), + "business_address": { + "street": intake.get("address_street", ""), + "city": intake.get("address_city", ""), + "state": intake.get("address_state", ""), + "zip": intake.get("address_zip", ""), + }, + "phone": intake.get("phone", ""), + "email": customer_email, + "entity_type": entity_type, + "requested_at": datetime.utcnow().isoformat(), + } + + # If we have a process agent API, submit directly + if PROCESS_AGENT_PARTNER.get("api_endpoint"): + success = self._submit_to_process_agent(designation) + if success: + self._send_confirmation_email(order_number, entity_name, dot_number, customer_email) + return [] + + # Otherwise create admin todo + todo_data = { + "order_number": order_number, + "service": self.SERVICE_NAME, + "designation": designation, + "current_boc3_status": boc3_status, + "steps": [ + "1. Submit BOC-3 designation request to process agent partner", + f" Partner: {PROCESS_AGENT_PARTNER['name']}", + f" Email: {PROCESS_AGENT_PARTNER.get('contact_email', 'TBD')}", + "2. Include: DOT#, MC#, legal name, address for all 48 states + DC", + "3. Process agent files BOC-3 electronically with FMCSA", + "4. Verify filing at https://li-public.fmcsa.dot.gov/LIVIEW/pkg_carrquery.prc_carrlist", + "5. Send confirmation to client", + ], + } + + 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"BOC-3 Filing — {entity_name} (DOT {dot_number})", + "filing", + "high", + order_number, + self.SERVICE_SLUG, + f"File BOC-3 process agent designation for {entity_name}.\n" + f"DOT: {dot_number}\n" + f"MC/Docket: {docket_number}\n" + f"Type: {entity_type}\n" + f"Customer: {customer_email}\n\n" + f"Submit to process agent partner for electronic filing with FMCSA.", + json.dumps(todo_data), + )) + conn.commit() + conn.close() + LOG.info("[%s] Admin todo created for BOC-3 filing", order_number) + except Exception as exc: + LOG.error("[%s] Failed to create admin todo: %s", order_number, exc) + + # Send status email + self._send_status_email(order_number, entity_name, dot_number, customer_email) + + return [] + + def _check_boc3_status(self, dot_number: str) -> str: + """Check if carrier has a BOC-3 on file 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/" + f"{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", {}) + # BOC-3 status isn't directly in the API, but we can check + # if authority is active (requires BOC-3 + insurance on file) + common = carrier.get("commonAuthorityStatus", "N") + contract = carrier.get("contractAuthorityStatus", "N") + broker = carrier.get("brokerAuthorityStatus", "N") + + if common == "A" or contract == "A" or broker == "A": + return "Authority active (BOC-3 likely on file)" + else: + return "No active authority (BOC-3 may be needed)" + except Exception as exc: + return f"Could not check: {exc}" + + def _submit_to_process_agent(self, designation: dict) -> bool: + """Submit designation to process agent partner via API.""" + # TODO: Implement when partner API is available + LOG.warning("Process agent API not configured — falling back to admin todo") + return False + + def _send_status_email(self, order_number, entity_name, dot_number, customer_email): + """Send client an email that we're working on their BOC-3.""" + if not customer_email: + return + try: + import smtplib + from email.mime.text import MIMEText + + body = ( + f"Hi,\n\n" + f"We've received your BOC-3 process agent designation order for " + f"{entity_name} (DOT# {dot_number}).\n\n" + f"Order: {order_number}\n\n" + f"We're submitting your designation to our blanket process agent " + f"who covers all 48 contiguous states plus DC. Once filed with " + f"FMCSA, your operating authority will reflect the active BOC-3.\n\n" + f"This is typically completed within 1-2 business days.\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"BOC-3 Filing 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) + + def _send_confirmation_email(self, order_number, entity_name, dot_number, customer_email): + """Send confirmation that BOC-3 has been filed.""" + if not customer_email: + return + try: + import smtplib + from email.mime.text import MIMEText + + body = ( + f"Hi,\n\n" + f"Your BOC-3 process agent designation has been filed with FMCSA " + f"for {entity_name} (DOT# {dot_number}).\n\n" + f"Order: {order_number}\n\n" + f"Your process agent is now designated in all 48 contiguous states " + f"plus the District of Columbia. This designation remains active " + f"as long as your carrier account is maintained.\n\n" + f"You can verify your BOC-3 status at:\n" + f"https://li-public.fmcsa.dot.gov/LIVIEW/pkg_carrquery.prc_carrlist\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"BOC-3 Filed — {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] Confirmation email sent to %s", order_number, customer_email) + except Exception as exc: + LOG.warning("[%s] Failed to send confirmation email: %s", order_number, exc) diff --git a/scripts/workers/services/mcs150_update.py b/scripts/workers/services/mcs150_update.py new file mode 100644 index 0000000..8c76110 --- /dev/null +++ b/scripts/workers/services/mcs150_update.py @@ -0,0 +1,198 @@ +""" +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" + + 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)