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:
justin 2026-06-16 03:29:05 -05:00
parent bf69960e8c
commit aadf9f5bc1
2 changed files with 646 additions and 0 deletions

View file

@ -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.