new-site/scripts/workers/services/boc3_filing.py
justin 68e6b60951 fix: worker emails (localhost:25 -> SMTP relay) + create ERPNext SO on webhook payment
Two bugs found tracing Mitchell Allen's batch CB-95BA6C90 (5 DOT services, card):

1) Worker authorization/signing-link/status emails were sent via
   smtplib.SMTP('localhost', 25), which has no MTA in the workers container ->
   every send failed '[Errno 111] Connection refused', so customers never got
   their e-sign links and orders sat 'awaiting client signature' forever. Routed
   all 9 hardcoded localhost:25 sites (state_trucking, mcs150_update, boc3_filing,
   hazmat_phmsa, mailbox_setup, dot_esign, completion_emails) through the
   authenticated SMTP relay (SMTP_HOST/PORT/STARTTLS/login) + added a shared
   worker_email.send_worker_email helper.

2) The ERPNext Sales Order for compliance/compliance_batch was only created in
   the /checkout/create-session endpoint, but CARD orders confirm via the Stripe
   WEBHOOK -> handlePaymentComplete, which never created the SO. Result: every
   webhook-confirmed order had erpnext_sales_order=NULL and workers logged
   'Sales Order not found 404' then built from PG. Added idempotent
   ensureComplianceSalesOrder() to handlePaymentComplete so ALL payment methods
   (card-webhook, PayPal, crypto) create + link the SO.
2026-06-09 14:40:46 -05:00

422 lines
19 KiB
Python

"""
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
from scripts.workers.telegram_notify import notify_fulfillment_todo
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 authority/BOC-3 status (structured) and branch on it.
auth = self._get_authority_state(dot_number)
boc3_status = auth["summary"]
branch = auth["branch"]
# Branch-specific follow-ups. These are surfaced for upsell-approve on the
# order timeline (customer/admin confirms + pays) — NEVER auto-charged.
recommended_followups: list[dict] = []
branch_steps: list[str] = []
if branch == "active":
# Default behavior: just file/refresh the BOC-3.
branch_steps = ["Authority is ACTIVE — file/refresh BOC-3 only."]
elif branch == "pending":
branch_steps = [
"Authority is PENDING — BOC-3 can be filed now (parallel OK).",
"Authority will NOT activate until active insurance (BMC-91/BMC-34) is on file"
" AND the ~21-day vetting/protest window passes.",
]
recommended_followups.append({
"type": "insurance_reminder",
"title": "Confirm active insurance is on file",
"reason": "Pending authority needs insurance + the 21-day vetting "
"window before it activates.",
"service_slug": None,
})
elif branch == "revoked":
branch_steps = [
"Authority is REVOKED/INACTIVE — BOC-3 alone does NOT reinstate.",
"Recommend reinstatement (OP-1 reinstatement + $80 gov fee).",
]
recommended_followups.append({
"type": "upsell",
"title": "Reinstate operating authority",
"reason": "Authority is revoked; a BOC-3 cannot activate revoked "
"authority. Reinstatement (OP-1 + $80 FMCSA fee) is required.",
"service_slug": "mc-authority",
})
elif branch == "none":
branch_steps = [
"NO operating authority on file (USDOT only) — BOC-3 has nothing to attach to.",
"MC operating authority is likely needed FIRST; do NOT file BOC-3 in isolation.",
]
recommended_followups.append({
"type": "upsell",
"title": "Apply for MC operating authority first",
"reason": "A BOC-3 designates process agents for an operating "
"authority. With USDOT only, authority must be obtained first.",
"service_slug": "mc-authority",
})
else:
branch_steps = [
f"Authority status UNKNOWN ({boc3_status}) — verify manually before filing.",
]
# 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,
"authority_state": auth,
"recommended_followups": recommended_followups,
"steps": branch_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", ""))
todo_title = f"BOC-3 Filing — {entity_name} (DOT {dot_number})"
todo_description = (
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"Authority status: {boc3_status}\n"
f"Customer: {customer_email}\n\n"
+ ("Recommended follow-ups (upsell-approve, not auto-charged):\n"
+ "\n".join(f" - {f['title']}: {f['reason']}"
for f in recommended_followups) + "\n\n"
if recommended_followups else "")
+ f"Submit to process agent partner for electronic filing with FMCSA."
)
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')
""", (
todo_title,
"filing",
"high",
order_number,
self.SERVICE_SLUG,
todo_description,
json.dumps(todo_data),
))
conn.commit()
notify_fulfillment_todo(
title=todo_title,
order_number=order_number,
service_slug=self.SERVICE_SLUG,
priority="high",
description=todo_description,
)
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:
"""Human-readable summary string (kept for backward compat / emails)."""
auth = self._get_authority_state(dot_number)
return auth.get("summary", "Could not determine authority status")
def _get_authority_state(self, dot_number: str) -> dict:
"""
Return structured authority state from FMCSA QC API.
Keys: common/contract/broker (raw status codes A/I/P/N/None),
any_active (bool), any_pending (bool), any_revoked (bool),
has_any_authority (bool), branch (str), summary (str).
Branch is one of: active | pending | revoked | none | unknown.
"""
result = {
"common": None, "contract": None, "broker": None,
"any_active": False, "any_pending": False, "any_revoked": False,
"has_any_authority": False, "branch": "unknown",
"summary": "Could not determine authority status",
}
try:
import urllib.request
api_key = os.environ.get("FMCSA_API_KEY", "")
if not api_key:
result["summary"] = "API key not configured"
return result
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", {})
common = carrier.get("commonAuthorityStatus")
contract = carrier.get("contractAuthorityStatus")
broker = carrier.get("brokerAuthorityStatus")
statuses = [s for s in (common, contract, broker) if s]
result.update(common=common, contract=contract, broker=broker)
# FMCSA codes: A=active, I=inactive, P=pending, N=none/not authorized.
result["any_active"] = any(s == "A" for s in statuses)
result["any_pending"] = any(s == "P" for s in statuses)
result["any_revoked"] = any(s == "I" for s in statuses)
result["has_any_authority"] = any(s in ("A", "I", "P") for s in statuses)
if result["any_active"]:
result["branch"] = "active"
result["summary"] = "Authority active (BOC-3 likely on file)"
elif result["any_pending"]:
result["branch"] = "pending"
result["summary"] = "Authority pending (needs BOC-3 + insurance to activate)"
elif result["any_revoked"]:
result["branch"] = "revoked"
result["summary"] = "Authority revoked/inactive (reinstatement likely needed)"
else:
result["branch"] = "none"
result["summary"] = "No operating authority on file (USDOT only)"
except Exception as exc:
result["summary"] = f"Could not check: {exc}"
return result
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
import os as _smtp_os
with smtplib.SMTP(_smtp_os.getenv("SMTP_HOST", "co.carrierone.com"), int(_smtp_os.getenv("SMTP_PORT", "587")), timeout=30) as s:
s.starttls()
_u, _p = _smtp_os.getenv("SMTP_USER", ""), _smtp_os.getenv("SMTP_PASS", "")
if _u and _p:
s.login(_u, _p)
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
import os as _smtp_os
with smtplib.SMTP(_smtp_os.getenv("SMTP_HOST", "co.carrierone.com"), int(_smtp_os.getenv("SMTP_PORT", "587")), timeout=30) as s:
s.starttls()
_u, _p = _smtp_os.getenv("SMTP_USER", ""), _smtp_os.getenv("SMTP_PASS", "")
if _u and _p:
s.login(_u, _p)
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)