feat(ucr): Playwright auto-filing for UCR registration on approval
Adds scripts/workers/services/ucr_playwright.py — a UCR.gov National Registration System automation that, given a USDOT + fleet size, runs the register/pay flow, pays the federal UCR fee with the matched PW filing card (Relay/Stripe Issuing), and captures a confirmation screenshot + number. Conventions match boc3_playwright / fmcsa_web_submitter: dev-mode dry-run guard, undetected (patchright) browser, CAPTCHA detection, screenshot evidence, dataclass result. Safety: verifies the displayed fee against the federal schedule before paying and refuses to auto-charge a surprising amount (UCR_MAX_AUTO_FEE_USD) — falls back to manual filing instead. Wires it into MCS150UpdateHandler: when an approved (admin_approved) order has slug ucr-registration, _file_ucr_registration runs the automation, uploads the confirmation screenshot to MinIO, records filing_status + confirmation, and sets fulfillment_status=completed on success. On CAPTCHA / fee-mismatch / failure it reverts to ready_to_file with a high-priority 'file manually' todo. This replaces the old behavior where approving a UCR just sat at authorization_signed.
This commit is contained in:
parent
bf69960e8c
commit
aadf9f5bc1
2 changed files with 646 additions and 0 deletions
|
|
@ -338,6 +338,15 @@ class MCS150UpdateHandler:
|
|||
order_number)
|
||||
return [minio_path] if minio_path else []
|
||||
|
||||
# Step 4c: ADMIN-ASSISTED AUTO-FILING. Some admin-assisted services have
|
||||
# a real automated filing path even though they don't produce an MCS-150
|
||||
# form. Once an admin has approved (admin_approved=True), run the
|
||||
# automation here instead of just creating a manual todo. Currently:
|
||||
# - ucr-registration -> ucr.gov National Registration System
|
||||
if slug == "ucr-registration":
|
||||
return self._file_ucr_registration(
|
||||
order_number, entity_name, dot_number, intake, customer_email)
|
||||
|
||||
# Step 5: Submit electronically (3x web → fax fallback)
|
||||
# GUARD: Skip actual submission in dev/test environments
|
||||
is_production = os.environ.get("NODE_ENV") == "production" or os.environ.get("ENV") == "production"
|
||||
|
|
@ -861,6 +870,169 @@ class MCS150UpdateHandler:
|
|||
except Exception as exc: # noqa: BLE001
|
||||
LOG.warning("[%s] Failed to mark intake validated: %s", order_number, exc)
|
||||
|
||||
def _file_ucr_registration(self, order_number, entity_name, dot_number,
|
||||
intake, customer_email) -> list:
|
||||
"""Run the UCR.gov Playwright automation for an approved UCR order, then
|
||||
persist the result. On success -> completed (+ confirmation + screenshot
|
||||
evidence). On CAPTCHA / fee-mismatch / failure -> ready_to_file with a
|
||||
high-priority 'file manually' todo so a human takes over."""
|
||||
import asyncio
|
||||
LOG.info("[%s] Auto-filing UCR registration via ucr.gov", order_number)
|
||||
|
||||
# Resolve the customer's payment method so we charge the right card.
|
||||
payment_method = "card"
|
||||
try:
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT payment_method FROM compliance_orders WHERE order_number=%s",
|
||||
(order_number,))
|
||||
row = cur.fetchone()
|
||||
if row and row[0]:
|
||||
payment_method = row[0]
|
||||
conn.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.warning("[%s] Could not read payment_method (default card): %s", order_number, exc)
|
||||
|
||||
try:
|
||||
from scripts.workers.services.ucr_playwright import UCRRegistration
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.error("[%s] UCR adapter import failed: %s", order_number, exc)
|
||||
self._set_fulfillment_status(order_number, "ready_to_file")
|
||||
self._create_admin_review_todo(
|
||||
order_number, entity_name, dot_number, "ucr-registration", None,
|
||||
customer_email, client_signed=False)
|
||||
return []
|
||||
|
||||
data = {
|
||||
"dot_number": dot_number,
|
||||
"legal_name": intake.get("legal_name", entity_name),
|
||||
"power_units": intake.get("power_units", ""),
|
||||
"email": intake.get("email") or customer_email,
|
||||
"phone": intake.get("phone", ""),
|
||||
"address_street": intake.get("address_street", ""),
|
||||
"address_city": intake.get("address_city", ""),
|
||||
"address_state": intake.get("address_state", ""),
|
||||
"address_zip": intake.get("address_zip", ""),
|
||||
}
|
||||
try:
|
||||
adapter = UCRRegistration()
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
result = loop.run_until_complete(
|
||||
adapter.file_ucr(data, order_number=order_number, payment_method=payment_method))
|
||||
finally:
|
||||
loop.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.error("[%s] UCR automation crashed: %s", order_number, exc)
|
||||
self._set_fulfillment_status(order_number, "ready_to_file")
|
||||
self._create_admin_review_todo(
|
||||
order_number, entity_name, dot_number, "ucr-registration", None,
|
||||
customer_email, client_signed=False)
|
||||
return []
|
||||
|
||||
# Upload the confirmation screenshot to MinIO as durable evidence.
|
||||
evidence = {}
|
||||
if result.screenshot_path and os.path.exists(result.screenshot_path):
|
||||
try:
|
||||
from minio import Minio
|
||||
mc = Minio(
|
||||
f"{os.environ.get('MINIO_ENDPOINT', 'minio')}:{os.environ.get('MINIO_PORT', '9000')}",
|
||||
access_key=os.environ.get("MINIO_ACCESS_KEY", ""),
|
||||
secret_key=os.environ.get("MINIO_SECRET_KEY", ""),
|
||||
secure=os.environ.get("MINIO_SECURE", "false").lower() == "true",
|
||||
)
|
||||
bucket = os.environ.get("MINIO_BUCKET", "performancewest")
|
||||
key = f"filings/ucr-registration/{order_number}/evidence/ucr_confirmation.png"
|
||||
mc.fput_object(bucket, key, result.screenshot_path, content_type="image/png")
|
||||
evidence["confirmation_screenshot"] = key
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.warning("[%s] Could not upload UCR screenshot: %s", order_number, exc)
|
||||
|
||||
# Persist filing_status on the order.
|
||||
filing_status = {
|
||||
"filing_method": "ucr_gov_web",
|
||||
"filing_success": bool(result.success),
|
||||
"submitted_at": datetime.now(timezone.utc).isoformat() if result.success else None,
|
||||
"manual_confirmation": result.confirmation_number or None,
|
||||
"fee_paid_usd": result.fee_paid_usd or None,
|
||||
"dry_run": bool(result.dry_run),
|
||||
"evidence": evidence,
|
||||
"error": result.error or None,
|
||||
}
|
||||
try:
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE compliance_orders SET intake_data = jsonb_set("
|
||||
"COALESCE(intake_data,'{}'::jsonb), '{filing_status}', %s::jsonb), "
|
||||
"updated_at=now() WHERE order_number=%s",
|
||||
(json.dumps(filing_status), order_number),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.error("[%s] Failed to persist UCR filing_status: %s", order_number, exc)
|
||||
|
||||
svc_label = self._service_label("ucr-registration")
|
||||
if result.success:
|
||||
self._set_fulfillment_status(order_number, "completed")
|
||||
conf = result.confirmation_number or "(see receipt screenshot)"
|
||||
self._notify_todo(
|
||||
order_number, "ucr-registration",
|
||||
f"{svc_label} FILED — {entity_name} (DOT {dot_number})",
|
||||
"low",
|
||||
(f"{svc_label} for {entity_name} (DOT {dot_number}).\n"
|
||||
f"Status: FILED on ucr.gov{' (DEV dry-run)' if result.dry_run else ''}.\n"
|
||||
f"Confirmation: {conf}\n"
|
||||
f"Fee paid: ${result.fee_paid_usd:.2f}\n"
|
||||
f"Customer: {customer_email}"),
|
||||
)
|
||||
self._send_status_email(order_number, entity_name, dot_number, customer_email, "ucr-registration")
|
||||
LOG.info("[%s] UCR filed (completed). conf=%s", order_number, conf)
|
||||
else:
|
||||
# Could not auto-file (CAPTCHA, fee mismatch, error) -> back to the
|
||||
# manual-filing queue with a clear, high-priority todo.
|
||||
self._set_fulfillment_status(order_number, "ready_to_file")
|
||||
reason = "CAPTCHA — needs manual filing" if result.captcha_hit else (result.error or "automation could not confirm")
|
||||
self._notify_todo(
|
||||
order_number, "ucr-registration",
|
||||
f"{svc_label} — FILE MANUALLY — {entity_name} (DOT {dot_number})",
|
||||
"high",
|
||||
(f"{svc_label} for {entity_name} (DOT {dot_number}).\n"
|
||||
f"Status: AUTO-FILE FAILED — {reason}.\n"
|
||||
f"ACTION: file manually on ucr.gov, then mark the order completed "
|
||||
f"in the admin dashboard (enter the confirmation #).\n"
|
||||
f"Customer: {customer_email}"),
|
||||
)
|
||||
LOG.warning("[%s] UCR auto-file failed: %s", order_number, reason)
|
||||
return []
|
||||
|
||||
def _notify_todo(self, order_number, slug, title, priority, description):
|
||||
"""Create an admin_todos row + Telegram fulfillment notification."""
|
||||
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, 'filing', %s, %s, %s, %s, %s, 'pending')""",
|
||||
(title, priority, order_number, slug, description,
|
||||
json.dumps({"order_number": order_number})),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.error("[%s] Failed to create todo: %s", order_number, exc)
|
||||
try:
|
||||
notify_fulfillment_todo(title=title, order_number=order_number,
|
||||
service_slug=slug, priority=priority,
|
||||
description=description)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
LOG.warning("[%s] Telegram notify failed: %s", order_number, exc)
|
||||
|
||||
def _create_admin_review_todo(self, order_number, entity_name, dot_number,
|
||||
slug, minio_path, customer_email, client_signed):
|
||||
"""High-priority admin todo: verify the prepared filing BEFORE submission.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue