mcs150: census-prefilled intake-completion flow + completeness gate
Closes the data gap for orders that bypass the full intake (e.g. the DOT compliance-remediation pipeline) and for all MCS-150 variants: - Worker intake-completeness gate (mcs150_update): before filling, check the customer-required operational fields the FMCSA census cannot supply (operation classification, cargo, CURRENT annual mileage, email; plus signer/address for new-registration/reactivation, and states-of-operation for 150B hazmat). If missing, email the customer a census-pre-filled intake link and hold the order at fulfillment_status='awaiting_intake' with an admin todo, instead of fabricating a blank filing. The existing intake PUT endpoint already re-dispatches the worker on submit, so filing auto-resumes. - Intake wizard (Wizard.astro): when resuming ?order=CO-xxx for a DOT/MCS order, seed still-empty fields from the FMCSA census (name/address/fleet/interstate) so the customer only confirms the operational details. - /api/v1/dot/census now also returns total_drivers + a normalized carrier_operation_code for the prefill. - MCS150Step.astro extended to collect every field the filler needs across all variants: mailing address, cdl_drivers, primary_vehicle_type, reason_for_filing, usdot_revoked, cell/fax, hazmat-safety-permit block (needs_hmsp, operating states, security plan), and intermodal-equipment provider counts; all prefill from intake_data. verify_mcs150_variants.py covers 150/150B/150C end-to-end (ALL PASS).
This commit is contained in:
parent
38739e023c
commit
a3aeedd716
4 changed files with 439 additions and 2 deletions
|
|
@ -124,6 +124,23 @@ class MCS150UpdateHandler:
|
|||
LOG.info("[%s] Backfilled signer_name from signed certification: %s",
|
||||
order_number, signed_name)
|
||||
|
||||
# INTAKE-COMPLETENESS GATE. The FMCSA census gives us the carrier's
|
||||
# registered name/address/fleet, but it cannot tell us the operational
|
||||
# details the MCS-150 requires the filer to confirm/update: the
|
||||
# operation classification (Q23), cargo types (Q24), current annual
|
||||
# mileage (Q21 -- must be CURRENT), and a contact email. If those are
|
||||
# missing we must NOT fabricate a filing; instead we ask the customer to
|
||||
# complete a short, census-pre-filled intake and hold the order until
|
||||
# they do. (New-registration / reactivation flows that have not yet
|
||||
# signed also route through here.)
|
||||
missing = self._missing_intake_fields(slug, intake)
|
||||
if missing and not client_approved and not admin_approved:
|
||||
self._request_intake_completion(
|
||||
order_number, entity_name, customer_email, dot_number, missing)
|
||||
LOG.info("[%s] Held for customer intake completion; missing=%s",
|
||||
order_number, missing)
|
||||
return []
|
||||
|
||||
# Step 1: Fill the official MCS-150 PDF
|
||||
pdf_path = None
|
||||
try:
|
||||
|
|
@ -355,6 +372,117 @@ class MCS150UpdateHandler:
|
|||
|
||||
return [minio_path] if minio_path else []
|
||||
|
||||
def _missing_intake_fields(self, slug: str, intake: dict) -> list:
|
||||
"""Return the customer-required intake fields still missing for this
|
||||
service. These are the operational details the FMCSA census cannot
|
||||
supply and that the MCS-150 requires the filer to confirm/update.
|
||||
"""
|
||||
# Per-variant required operational fields.
|
||||
base_required = ["carrier_operation", "interstate_intrastate",
|
||||
"annual_miles", "email"]
|
||||
# Cargo only applies to carriers (not pure intermodal-equipment providers).
|
||||
if not intake.get("is_intermodal_equipment_provider"):
|
||||
base_required.append("cargo_types")
|
||||
# New registrations / reactivations also need the signer + address.
|
||||
if slug in ("dot-registration", "usdot-reactivation", "dot-full-compliance"):
|
||||
base_required += ["signer_name", "signer_title", "address_street",
|
||||
"address_city", "address_state", "address_zip"]
|
||||
# Hazmat safety-permit (150B) needs states of operation.
|
||||
if intake.get("hazmat") == "yes" and intake.get("needs_hmsp"):
|
||||
base_required.append("operating_states")
|
||||
|
||||
missing = []
|
||||
for f in base_required:
|
||||
v = intake.get(f)
|
||||
if v in (None, "", [], {}):
|
||||
missing.append(f)
|
||||
return missing
|
||||
|
||||
def _request_intake_completion(self, order_number, entity_name,
|
||||
customer_email, dot_number, missing):
|
||||
"""Email the customer a census-pre-filled intake link and create a
|
||||
low-priority admin todo noting we're waiting on intake completion."""
|
||||
domain = os.environ.get("PUBLIC_SITE_URL", "https://performancewest.net").rstrip("/")
|
||||
intake_url = f"{domain}/order/dot-compliance?order={order_number}"
|
||||
|
||||
# Customer email (no paper/mail mechanics; public form names are fine).
|
||||
try:
|
||||
from scripts.workers.worker_email import send_worker_email
|
||||
label = {
|
||||
"carrier_operation": "how your company operates (for-hire, private, etc.)",
|
||||
"interstate_intrastate": "interstate vs intrastate operation",
|
||||
"annual_miles": "your current annual mileage",
|
||||
"cargo_types": "the types of cargo you haul",
|
||||
"email": "a contact email address",
|
||||
"operating_states": "the states you operate in",
|
||||
"signer_name": "the name of the company officer signing",
|
||||
"signer_title": "the officer's title",
|
||||
}
|
||||
needed = ", ".join(label.get(m, m.replace("_", " ")) for m in missing)
|
||||
subject = f"Action needed: confirm your MCS-150 details (DOT {dot_number})"
|
||||
text = (
|
||||
f"Hi,\n\n"
|
||||
f"We're ready to prepare the MCS-150 update for {entity_name} "
|
||||
f"(USDOT {dot_number}). We've pre-filled everything we can from "
|
||||
f"your current FMCSA record. To finish, we just need you to "
|
||||
f"confirm a few current details: {needed}.\n\n"
|
||||
f"Please review and confirm here (it takes about 2 minutes):\n"
|
||||
f"{intake_url}\n\n"
|
||||
f"Once you submit, we'll prepare your filing for your signature "
|
||||
f"and handle the rest.\n\n"
|
||||
f"Thank you,\nPerformance West"
|
||||
)
|
||||
html = (
|
||||
f"<p>Hi,</p>"
|
||||
f"<p>We're ready to prepare the MCS-150 update for "
|
||||
f"<strong>{entity_name}</strong> (USDOT {dot_number}). We've "
|
||||
f"pre-filled everything we can from your current FMCSA record. "
|
||||
f"To finish, we just need you to confirm a few current details: "
|
||||
f"<strong>{needed}</strong>.</p>"
|
||||
f"<p><a href=\"{intake_url}\">Review and confirm your details</a> "
|
||||
f"(about 2 minutes).</p>"
|
||||
f"<p>Once you submit, we'll prepare your filing for your "
|
||||
f"signature and handle the rest.</p>"
|
||||
f"<p>Thank you,<br>Performance West</p>"
|
||||
)
|
||||
if customer_email:
|
||||
send_worker_email(customer_email, subject, html, text=text,
|
||||
cc="justin@performancewest.net")
|
||||
except Exception as exc:
|
||||
LOG.warning("[%s] Could not send intake-completion email: %s",
|
||||
order_number, exc)
|
||||
|
||||
# Hold the order + admin todo.
|
||||
self._set_fulfillment_status(order_number, "awaiting_intake")
|
||||
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, %s, %s, %s, %s, %s, %s, 'pending')""",
|
||||
(
|
||||
f"Awaiting MCS-150 intake -- {entity_name} (DOT {dot_number})",
|
||||
"intake", "normal", order_number, self.SERVICE_SLUG,
|
||||
(f"Order {order_number} is missing customer-required intake "
|
||||
f"fields: {', '.join(missing)}.\n"
|
||||
f"A census-pre-filled intake link was emailed to "
|
||||
f"{customer_email or 'the customer'}.\n"
|
||||
f"Intake link: {intake_url}\n"
|
||||
f"Filing auto-resumes once the customer submits."),
|
||||
json.dumps({"order_number": order_number, "dot_number": dot_number,
|
||||
"entity_name": entity_name, "awaiting_intake": True,
|
||||
"missing_fields": missing}),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as exc:
|
||||
LOG.warning("[%s] Could not create intake-pending todo: %s",
|
||||
order_number, exc)
|
||||
|
||||
def _restamp_signed_form(self, order_number: str, document_key: str) -> None:
|
||||
"""Point the signed esign record at ``document_key`` (the freshly filled
|
||||
form) and re-stamp the signature onto it, so the signed PDF an admin
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue