diff --git a/scripts/workers/services/boc3_filing.py b/scripts/workers/services/boc3_filing.py index 4199f00..bc04f8c 100644 --- a/scripts/workers/services/boc3_filing.py +++ b/scripts/workers/services/boc3_filing.py @@ -136,8 +136,59 @@ class BOC3FilingHandler: LOG.error("[%s] Missing DOT number", order_number) return [] - # Check current BOC-3 status - boc3_status = self._check_boc3_status(dot_number) + # 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 = { @@ -163,7 +214,9 @@ class BOC3FilingHandler: "service": self.SERVICE_NAME, "designation": designation, "current_boc3_status": boc3_status, - "steps": [ + "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']}", @@ -192,8 +245,13 @@ class BOC3FilingHandler: 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" - f"Submit to process agent partner for electronic filing with FMCSA.", + + ("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.", json.dumps(todo_data), )) conn.commit() @@ -208,12 +266,31 @@ class BOC3FilingHandler: return [] def _check_boc3_status(self, dot_number: str) -> str: - """Check if carrier has a BOC-3 on file via FMCSA API.""" + """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: - return "API key not configured" + result["summary"] = "API key not configured" + return result url = ( f"https://mobile.fmcsa.dot.gov/qc/services/carriers/" @@ -224,18 +301,33 @@ class BOC3FilingHandler: 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") + common = carrier.get("commonAuthorityStatus") + contract = carrier.get("contractAuthorityStatus") + broker = carrier.get("brokerAuthorityStatus") + statuses = [s for s in (common, contract, broker) if s] - if common == "A" or contract == "A" or broker == "A": - return "Authority active (BOC-3 likely on file)" + 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: - return "No active authority (BOC-3 may be needed)" + result["branch"] = "none" + result["summary"] = "No operating authority on file (USDOT only)" except Exception as exc: - return f"Could not check: {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."""