feat(boc3): authority-aware filing with upsell-approve follow-ups
_get_authority_state() returns structured FMCSA authority state; handle() branches on active/pending/revoked/none: - active: file/refresh BOC-3 (current behavior) - pending: file BOC-3 + insurance/21-day-vetting reminder - revoked: file + recommend reinstatement (mc-authority, never auto-charge) - none (USDOT only): flag MC authority needed first, do not file blindly recommended_followups + authority_state persisted in admin todo for upsell-approve on the order timeline.
This commit is contained in:
parent
cadff79bd6
commit
bbbfeaeaa1
1 changed files with 107 additions and 15 deletions
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue