capture client signature before filing signed DOT forms
Forms that legally require the client's signature were not being captured correctly: - MCS-150 handler created a perjury e-sign record but then submitted to FMCSA anyway, before the client signed. Now it gates submission: request the signature, hold, and only file when handle_esign_completed re-dispatches with client_approved=True. - MCS-150 e-sign links were signed with JWT_SECRET/ADMIN_JWT_SECRET, but the portal verifies with CUSTOMER_JWT_SECRET, so every link returned "Invalid portal link." New shared dot_esign helper signs with CUSTOMER_JWT_SECRET. - carrier-closeout (final MCS-150 Out of Business) and entity-dissolution (Articles of Dissolution + no-lawsuits/liens/judgments attestation) captured no signature at all. Both now request a signed attestation before the workflow proceeds. - mc-authority / emergency-temporary-authority now get a correctly labeled OP-1 applicant certification instead of an "MCS-150" record. Also fixes a latent dispatcher bug: order["service_slug"] was never set, so handlers sharing a class fell back to their default SERVICE_SLUG. This made entity-dissolution run the carrier-closeout branch and mc-authority/etc. look like mcs150-update. Now the resolved slug is injected into order_data. Portal e-sign page now renders the document-specific certification text from metadata.perjury_text (so the dissolution no-liabilities attestation and OP-1 cert are actually shown to the signer), not just a generic perjury line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
869bcac287
commit
02112facf5
5 changed files with 366 additions and 84 deletions
|
|
@ -74,10 +74,18 @@ class MCS150UpdateHandler:
|
|||
if isinstance(intake, str):
|
||||
intake = json.loads(intake)
|
||||
|
||||
slug = order_data.get("service_slug", self.SERVICE_SLUG)
|
||||
dot_number = intake.get("dot_number", "")
|
||||
entity_name = intake.get("entity_name", order_data.get("customer_name", ""))
|
||||
customer_email = order_data.get("customer_email", "")
|
||||
|
||||
# The client signs the perjury certification before we file. When they
|
||||
# sign, handle_esign_completed re-dispatches this handler with
|
||||
# client_approved=True so we proceed past the signing checkpoint.
|
||||
from scripts.workers.services.dot_esign import requires_signature, request_dot_esign
|
||||
needs_signature = requires_signature(slug)
|
||||
client_approved = bool(order_data.get("client_approved"))
|
||||
|
||||
# Validate required fields
|
||||
if not dot_number:
|
||||
LOG.error("[%s] Missing DOT number in intake data", order_number)
|
||||
|
|
@ -140,50 +148,27 @@ class MCS150UpdateHandler:
|
|||
except Exception as exc:
|
||||
LOG.warning("[%s] Could not retrieve photo ID: %s", order_number, exc)
|
||||
|
||||
# Step 4: Create e-sign record for perjury declaration
|
||||
# Customer must sign before we submit to FMCSA
|
||||
if pdf_path and minio_path:
|
||||
try:
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO esign_records (
|
||||
order_number, document_type, document_title, entity_name,
|
||||
document_minio_key, document_metadata,
|
||||
requires_perjury, status, expires_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s, TRUE, 'pending', NOW() + interval '7 days')
|
||||
ON CONFLICT (order_number, document_type)
|
||||
WHERE status IN ('pending', 'signed') DO NOTHING
|
||||
""", (
|
||||
order_number,
|
||||
"mcs150",
|
||||
"MCS-150 Biennial Update — Certification Under Penalty of Perjury",
|
||||
entity_name,
|
||||
minio_path,
|
||||
json.dumps({
|
||||
"dot_number": dot_number,
|
||||
"form_type": "mcs150",
|
||||
"perjury_text": (
|
||||
"I hereby certify under penalty of perjury that the information "
|
||||
"contained in this MCS-150 form is true and correct to the best "
|
||||
"of my knowledge and belief. I understand that making a false "
|
||||
"statement is punishable under 18 U.S.C. § 1001."
|
||||
),
|
||||
}),
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
LOG.info("[%s] E-sign record created for MCS-150 perjury declaration", order_number)
|
||||
# Step 4: SIGNATURE GATE. If this form needs the client's signed
|
||||
# certification and they haven't signed yet, request the signature and
|
||||
# STOP before filing. We never submit a perjury certification to FMCSA
|
||||
# until the client has actually signed it.
|
||||
if needs_signature and not client_approved:
|
||||
request_dot_esign(
|
||||
order_number=order_number,
|
||||
slug=slug,
|
||||
entity_name=entity_name,
|
||||
customer_email=customer_email,
|
||||
dot_number=dot_number,
|
||||
document_minio_key=minio_path or "",
|
||||
)
|
||||
LOG.info("[%s] Awaiting client signature before filing %s — not submitting yet",
|
||||
order_number, slug)
|
||||
self._create_pending_signature_todo(
|
||||
order_number, entity_name, dot_number, slug, minio_path, customer_email)
|
||||
return [minio_path] if minio_path else []
|
||||
|
||||
# Send e-sign link to customer
|
||||
self._send_esign_email(order_number, entity_name, dot_number, customer_email)
|
||||
|
||||
# NOTE: The filing will be triggered by the esign_completed handler
|
||||
# after the customer signs. For now, also proceed with submission
|
||||
# since many orders may not have esign set up yet.
|
||||
except Exception as exc:
|
||||
LOG.error("[%s] E-sign setup failed (proceeding anyway): %s", order_number, exc)
|
||||
# Past this point: either no signature is required for this service, or
|
||||
# the client has signed (re-dispatched with client_approved=True).
|
||||
|
||||
# Step 5: Submit electronically (3x web → fax fallback)
|
||||
# GUARD: Skip actual submission in dev/test environments
|
||||
|
|
@ -320,48 +305,37 @@ class MCS150UpdateHandler:
|
|||
except Exception as exc:
|
||||
return f"Could not check: {exc}"
|
||||
|
||||
def _send_esign_email(self, order_number, entity_name, dot_number, customer_email):
|
||||
"""Send e-sign link for MCS-150 perjury declaration."""
|
||||
if not customer_email:
|
||||
return
|
||||
def _create_pending_signature_todo(self, order_number, entity_name, dot_number,
|
||||
slug, minio_path, customer_email):
|
||||
"""Low-priority admin todo noting we're waiting on the client's signature."""
|
||||
try:
|
||||
import smtplib
|
||||
import jwt
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
secret = os.environ.get("JWT_SECRET", os.environ.get("ADMIN_JWT_SECRET", ""))
|
||||
token = jwt.encode(
|
||||
{"order_id": order_number, "order_type": "mcs150", "email": customer_email},
|
||||
secret, algorithm="HS256",
|
||||
)
|
||||
domain = os.environ.get("DOMAIN", "performancewest.net")
|
||||
esign_url = f"https://{domain}/portal/esign?token={token}"
|
||||
|
||||
body = (
|
||||
f"Hi,\n\n"
|
||||
f"Your MCS-150 Biennial Update for {entity_name} (DOT# {dot_number}) "
|
||||
f"has been prepared and is ready for your signature.\n\n"
|
||||
f"Federal law requires your certification under penalty of perjury "
|
||||
f"before we can submit this form to FMCSA.\n\n"
|
||||
f"Please review and sign here:\n{esign_url}\n\n"
|
||||
f"This link expires in 7 days.\n\n"
|
||||
f"Once you sign, we will submit the form to FMCSA electronically "
|
||||
f"and provide you with a Certificate of Filing.\n\n"
|
||||
f"Questions? Call (888) 411-0383.\n\n"
|
||||
f"Performance West Inc.\n"
|
||||
)
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg["Subject"] = f"Action Required: Sign Your MCS-150 — {entity_name} (DOT {dot_number})"
|
||||
msg["From"] = "noreply@performancewest.net"
|
||||
msg["To"] = customer_email
|
||||
|
||||
with smtplib.SMTP("localhost", 25) as s:
|
||||
s.sendmail(msg["From"], [customer_email], msg.as_string())
|
||||
|
||||
LOG.info("[%s] E-sign email sent to %s", order_number, customer_email)
|
||||
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 client signature — {entity_name} (DOT {dot_number})",
|
||||
"filing", "low", order_number, slug,
|
||||
f"{slug} for {entity_name} (DOT {dot_number}).\n"
|
||||
f"Status: AWAITING CLIENT SIGNATURE before filing.\n"
|
||||
f"Signing link emailed to {customer_email}.\n"
|
||||
f"PDF: {minio_path or 'not generated'}\n"
|
||||
f"Filing auto-resumes once the client signs.",
|
||||
json.dumps({"order_number": order_number, "dot_number": dot_number,
|
||||
"entity_name": entity_name, "awaiting_signature": True}),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
LOG.info("[%s] Pending-signature todo created", order_number)
|
||||
except Exception as exc:
|
||||
LOG.warning("[%s] Failed to send e-sign email: %s", order_number, exc)
|
||||
LOG.warning("[%s] Failed to create pending-signature todo: %s", order_number, exc)
|
||||
|
||||
def _send_status_email(self, order_number, entity_name, dot_number, customer_email):
|
||||
"""Send client an email that we're working on their update."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue