""" 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 asyncio import json import logging import os from datetime import datetime LOG = logging.getLogger("workers.services.boc3_filing") # Process agent partner: Registered Agents Inc / Process Agent LLC # ProcessAgent.com is a subsidiary of Registered Agents Inc. # They own commercial offices in every US state (blanket agent). # Cost: $25/year per carrier. No API — file via Playwright on their site. # Phone: (406) 300-4044 PROCESS_AGENT_PARTNER = { "name": "Process Agent LLC (Registered Agents Inc)", "website": "https://www.processagent.com", "phone": "(406) 300-4044", "cost_cents": 2500, # $25/year "automation": "playwright", # No API — automate via browser } class BOC3FilingHandler: """Handle BOC-3 process agent designation orders.""" SERVICE_SLUG = "boc3-filing" SERVICE_NAME = "BOC-3 Process Agent Filing" async def process(self, order_data: dict) -> list[str]: """Entry point called by job_server. Tries Playwright, falls back to handle().""" order_number = order_data.get("order_number", order_data.get("name", "")) intake = order_data.get("intake_data") or {} if isinstance(intake, str): intake = json.loads(intake) dot_number = intake.get("dot_number", "") customer_email = order_data.get("customer_email", "") # Try Playwright automation if credentials are configured if dot_number and os.environ.get("PW_CARD_NUMBER") and os.environ.get("BOC3_ACCOUNT_PASSWORD"): try: from .boc3_playwright import BOC3ProcessAgent adapter = BOC3ProcessAgent() result = await adapter.file_boc3({ "dot_number": dot_number, "docket_number": intake.get("docket_number", ""), "legal_name": intake.get("entity_name", order_data.get("customer_name", "")), "entity_type": intake.get("entity_type", "carrier"), "contact": { "first_name": (order_data.get("customer_name") or "").split()[0] if order_data.get("customer_name") else "", "last_name": " ".join((order_data.get("customer_name") or "").split()[1:]), "phone": intake.get("phone", ""), "street": intake.get("address_street", ""), "city": intake.get("address_city", ""), "state": intake.get("address_state", ""), "zip": intake.get("address_zip", ""), }, "email": customer_email, }) if result.success: LOG.info("[%s] BOC-3 filed via Playwright! Order: %s", order_number, result.order_id) self._send_confirmation_email( order_number, intake.get("entity_name", order_data.get("customer_name", "")), dot_number, customer_email, ) return [] elif result.captcha_hit: LOG.warning("[%s] CAPTCHA on processagent.com — falling back to admin todo", order_number) else: LOG.warning("[%s] Playwright filing failed: %s — admin todo", order_number, result.error) except Exception as exc: LOG.warning("[%s] Playwright error: %s — admin todo", order_number, exc) # Fall back to manual admin todo return self.handle(order_data, order_number) 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(), } # Create admin todo for manual filing (Playwright attempt already made in process()) todo_data = { "order_number": order_number, "service": self.SERVICE_NAME, "designation": designation, "current_boc3_status": boc3_status, "steps": [ "1. Go to https://www.processagent.com/order", "2. Submit BOC-3 order ($25) with carrier's DOT#, MC#, legal name, address", f" Partner: {PROCESS_AGENT_PARTNER['name']}", "3. Process Agent LLC files BOC-3 electronically with FMCSA (1-5 business days)", "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 _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)