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.
412 lines
18 KiB
Python
412 lines
18 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
|
|
|
|
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)
|