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
|
|
@ -1141,6 +1141,14 @@ def handle_process_compliance_service(payload: dict) -> dict:
|
||||||
if order_number:
|
if order_number:
|
||||||
order["order_number"] = order_number
|
order["order_number"] = order_number
|
||||||
|
|
||||||
|
# Expose the resolved service slug to the handler. Several services share one
|
||||||
|
# handler class (e.g. mc-authority / usdot-reactivation -> MCS150UpdateHandler,
|
||||||
|
# entity-dissolution -> CarrierCloseoutHandler), and they branch on the slug.
|
||||||
|
# Without this, order_data.get("service_slug") is absent and handlers fall back
|
||||||
|
# to their default SERVICE_SLUG, mis-identifying the ordered service.
|
||||||
|
if service_slug:
|
||||||
|
order["service_slug"] = service_slug
|
||||||
|
|
||||||
# Inject eSign approval flags from payload (set by handle_esign_completed)
|
# Inject eSign approval flags from payload (set by handle_esign_completed)
|
||||||
if payload.get("client_approved"):
|
if payload.get("client_approved"):
|
||||||
order["client_approved"] = True
|
order["client_approved"] = True
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,40 @@ class CarrierCloseoutHandler:
|
||||||
"Sequential wind-down steps:\n"
|
"Sequential wind-down steps:\n"
|
||||||
+ "\n".join(f" {i + 1}. {s}" for i, s in enumerate(steps))
|
+ "\n".join(f" {i + 1}. {s}" for i, s in enumerate(steps))
|
||||||
)
|
)
|
||||||
self._create_todo(order_number, intake, title, description, slug, priority="high")
|
|
||||||
|
# Signature gate. Both close-out (final MCS-150 "Out of Business") and
|
||||||
|
# entity dissolution require the client's signed authorization/attestation
|
||||||
|
# before we file anything. On first run we request the signature and hold;
|
||||||
|
# handle_esign_completed re-dispatches with client_approved=True once signed.
|
||||||
|
from scripts.workers.services.dot_esign import requires_signature, request_dot_esign
|
||||||
|
client_approved = bool(order_data.get("client_approved"))
|
||||||
|
customer_email = order_data.get("customer_email", "")
|
||||||
|
|
||||||
|
if requires_signature(slug) and not client_approved:
|
||||||
|
request_dot_esign(
|
||||||
|
order_number=order_number,
|
||||||
|
slug=slug,
|
||||||
|
entity_name=name,
|
||||||
|
customer_email=customer_email,
|
||||||
|
dot_number=dot if dot != "N/A" else "",
|
||||||
|
)
|
||||||
|
LOG.info("[%s] Awaiting client signature before %s — holding workflow", order_number, slug)
|
||||||
|
self._create_todo(
|
||||||
|
order_number, intake,
|
||||||
|
f"{title} [AWAITING CLIENT SIGNATURE]",
|
||||||
|
description + f"\n\nStatus: AWAITING CLIENT SIGNATURE — link emailed to {customer_email}."
|
||||||
|
"\nWorkflow auto-resumes once the client signs.",
|
||||||
|
slug, priority="low",
|
||||||
|
)
|
||||||
|
return [f"Awaiting signature: {title}"]
|
||||||
|
|
||||||
|
# Signed (or re-dispatched after signing) — queue the actionable workflow.
|
||||||
|
self._create_todo(
|
||||||
|
order_number, intake,
|
||||||
|
f"{title} [SIGNED — READY TO FILE]",
|
||||||
|
description + "\n\nStatus: Client signed the authorization. Proceed with filing.",
|
||||||
|
slug, priority="high",
|
||||||
|
)
|
||||||
return [f"Close-out workflow queued: {title}"]
|
return [f"Close-out workflow queued: {title}"]
|
||||||
|
|
||||||
def _create_todo(self, order_number, intake, title, description, slug, priority="normal"):
|
def _create_todo(self, order_number, intake, title, description, slug, priority="normal"):
|
||||||
|
|
|
||||||
255
scripts/workers/services/dot_esign.py
Normal file
255
scripts/workers/services/dot_esign.py
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
"""DOT/FMCSA e-signature helper.
|
||||||
|
|
||||||
|
Creates an `esign_records` row and emails the client a signing link, using the
|
||||||
|
local Postfix relay (localhost:25) like the other DOT status emails. This is the
|
||||||
|
DOT-side counterpart to telecom/esign_helper.py, but without the external
|
||||||
|
SMTP-auth dependency.
|
||||||
|
|
||||||
|
CRITICAL: portal links are signed with CUSTOMER_JWT_SECRET — the same secret the
|
||||||
|
API portal middleware (api/src/middleware/portalAuth.ts) verifies with. Signing
|
||||||
|
with any other secret yields an "Invalid portal link." error for the client.
|
||||||
|
|
||||||
|
Which DOT forms legally require the client's signature BEFORE we file:
|
||||||
|
- mcs150 forms (MCS-150 biennial / new USDOT / reactivation) — perjury cert
|
||||||
|
- operating authority (OP-1 / MC / emergency temporary authority) — applicant cert
|
||||||
|
- carrier close-out (final MCS-150 "Out of Business") — perjury cert
|
||||||
|
- entity dissolution (Articles of Dissolution) — no-liabilities attestation
|
||||||
|
|
||||||
|
UCR, drug-&-alcohol consortium enrollment, audit prep, and BOC-3 (signed by the
|
||||||
|
process agent, not the carrier) do NOT file a client-signed federal form, so they
|
||||||
|
skip signature capture entirely.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
LOG = logging.getLogger("workers.services.dot_esign")
|
||||||
|
|
||||||
|
_MCS150_PERJURY = (
|
||||||
|
"I certify under penalty of perjury that the information in this MCS-150 is "
|
||||||
|
"true and correct to the best of my knowledge and belief, and that I am "
|
||||||
|
"authorized to file it on behalf of the motor carrier. I understand that "
|
||||||
|
"making a false statement is punishable under 18 U.S.C. § 1001."
|
||||||
|
)
|
||||||
|
_OP1_CERT = (
|
||||||
|
"I certify that I am authorized to apply for operating authority on behalf of "
|
||||||
|
"the applicant, that the information provided is true and correct, and that the "
|
||||||
|
"applicant will comply with all applicable FMCSA safety and insurance "
|
||||||
|
"requirements. I understand that a false statement is punishable under "
|
||||||
|
"18 U.S.C. § 1001."
|
||||||
|
)
|
||||||
|
_CLOSEOUT_CERT = (
|
||||||
|
"I authorize Performance West Inc. to file a final MCS-150 marking this carrier "
|
||||||
|
"OUT OF BUSINESS and to deactivate the USDOT number and operating authority "
|
||||||
|
"listed above. I certify under penalty of perjury that the information is true "
|
||||||
|
"and correct and that I am authorized to wind down this motor carrier."
|
||||||
|
)
|
||||||
|
_DISSOLUTION_ATTEST = (
|
||||||
|
"I attest that, to the best of my knowledge, this entity has NO outstanding "
|
||||||
|
"lawsuits, liens, judgments, or unsatisfied creditor claims. I authorize "
|
||||||
|
"Performance West Inc. to file Articles of Dissolution and final returns on the "
|
||||||
|
"entity's behalf, and I certify that I am authorized to dissolve this entity. I "
|
||||||
|
"understand that dissolving an entity with unresolved liabilities may expose its "
|
||||||
|
"members or officers to personal liability."
|
||||||
|
)
|
||||||
|
|
||||||
|
# slug -> signing config. A slug absent from this map requires NO signature.
|
||||||
|
DOT_SIGNING: dict[str, dict] = {
|
||||||
|
"mcs150-update": {
|
||||||
|
"document_type": "mcs150",
|
||||||
|
"document_title": "MCS-150 Biennial Update — Certification Under Penalty of Perjury",
|
||||||
|
"perjury_text": _MCS150_PERJURY,
|
||||||
|
},
|
||||||
|
"dot-registration": {
|
||||||
|
"document_type": "mcs150",
|
||||||
|
"document_title": "New USDOT Registration (MCS-150) — Certification Under Penalty of Perjury",
|
||||||
|
"perjury_text": _MCS150_PERJURY,
|
||||||
|
},
|
||||||
|
"usdot-reactivation": {
|
||||||
|
"document_type": "mcs150",
|
||||||
|
"document_title": "USDOT Reactivation (MCS-150) — Certification Under Penalty of Perjury",
|
||||||
|
"perjury_text": _MCS150_PERJURY,
|
||||||
|
},
|
||||||
|
"dot-full-compliance": {
|
||||||
|
"document_type": "mcs150",
|
||||||
|
"document_title": "DOT Full Compliance (MCS-150) — Certification Under Penalty of Perjury",
|
||||||
|
"perjury_text": _MCS150_PERJURY,
|
||||||
|
},
|
||||||
|
"mc-authority": {
|
||||||
|
"document_type": "operating-authority",
|
||||||
|
"document_title": "Operating Authority Application (OP-1) — Applicant Certification",
|
||||||
|
"perjury_text": _OP1_CERT,
|
||||||
|
},
|
||||||
|
"emergency-temporary-authority": {
|
||||||
|
"document_type": "operating-authority",
|
||||||
|
"document_title": "Emergency Temporary Authority Request — Applicant Certification",
|
||||||
|
"perjury_text": _OP1_CERT,
|
||||||
|
},
|
||||||
|
"carrier-closeout": {
|
||||||
|
"document_type": "carrier-closeout",
|
||||||
|
"document_title": "Carrier Close-Out Authorization & Final MCS-150 (Out of Business)",
|
||||||
|
"perjury_text": _CLOSEOUT_CERT,
|
||||||
|
},
|
||||||
|
"entity-dissolution": {
|
||||||
|
"document_type": "entity-dissolution",
|
||||||
|
"document_title": "Entity Dissolution Authorization & No-Liabilities Attestation",
|
||||||
|
"perjury_text": _DISSOLUTION_ATTEST,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def requires_signature(slug: str) -> bool:
|
||||||
|
"""True if the DOT service files a form that needs the client's signature."""
|
||||||
|
return slug in DOT_SIGNING
|
||||||
|
|
||||||
|
|
||||||
|
def request_dot_esign(
|
||||||
|
order_number: str,
|
||||||
|
slug: str,
|
||||||
|
entity_name: str,
|
||||||
|
customer_email: str,
|
||||||
|
dot_number: str = "",
|
||||||
|
document_minio_key: str = "",
|
||||||
|
extra_metadata: dict | None = None,
|
||||||
|
expires_days: int = 7,
|
||||||
|
) -> int | None:
|
||||||
|
"""Create a pending esign record for a DOT form and email the signing link.
|
||||||
|
|
||||||
|
Idempotent per (order_number, document_type): re-running will not duplicate a
|
||||||
|
pending/signed record. Returns the esign_records.id, or None on failure.
|
||||||
|
"""
|
||||||
|
cfg = DOT_SIGNING.get(slug)
|
||||||
|
if not cfg:
|
||||||
|
return None # this service does not require a signature
|
||||||
|
if not customer_email:
|
||||||
|
LOG.warning("[%s] No customer email — cannot request signature", order_number)
|
||||||
|
return None
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
document_type = cfg["document_type"]
|
||||||
|
document_title = cfg["document_title"]
|
||||||
|
perjury_text = cfg["perjury_text"]
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"dot_number": dot_number,
|
||||||
|
"service_slug": slug,
|
||||||
|
"perjury_text": perjury_text,
|
||||||
|
}
|
||||||
|
if extra_metadata:
|
||||||
|
metadata.update(extra_metadata)
|
||||||
|
|
||||||
|
# 1. Upsert the pending record
|
||||||
|
esign_id = None
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
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() + (%s || ' days')::interval)
|
||||||
|
ON CONFLICT (order_number, document_type)
|
||||||
|
WHERE status IN ('pending', 'signed')
|
||||||
|
DO UPDATE SET
|
||||||
|
document_title = EXCLUDED.document_title,
|
||||||
|
entity_name = EXCLUDED.entity_name,
|
||||||
|
document_minio_key = EXCLUDED.document_minio_key,
|
||||||
|
document_metadata = EXCLUDED.document_metadata,
|
||||||
|
expires_at = EXCLUDED.expires_at,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
order_number, document_type, document_title, entity_name,
|
||||||
|
document_minio_key, json.dumps(metadata), str(expires_days),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
esign_id = row[0] if row else None
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.error("[%s] Failed to create esign record (%s): %s", order_number, document_type, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 2. Email the signing link (signed with CUSTOMER_JWT_SECRET to match the portal)
|
||||||
|
try:
|
||||||
|
_send_signing_email(
|
||||||
|
order_number=order_number,
|
||||||
|
document_type=document_type,
|
||||||
|
document_title=document_title,
|
||||||
|
entity_name=entity_name,
|
||||||
|
dot_number=dot_number,
|
||||||
|
customer_email=customer_email,
|
||||||
|
expires_days=expires_days,
|
||||||
|
)
|
||||||
|
LOG.info("[%s] Signing link sent to %s (%s)", order_number, customer_email, document_type)
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.warning("[%s] Could not send signing email (record exists): %s", order_number, exc)
|
||||||
|
|
||||||
|
return esign_id
|
||||||
|
|
||||||
|
|
||||||
|
def _send_signing_email(
|
||||||
|
order_number: str,
|
||||||
|
document_type: str,
|
||||||
|
document_title: str,
|
||||||
|
entity_name: str,
|
||||||
|
dot_number: str,
|
||||||
|
customer_email: str,
|
||||||
|
expires_days: int,
|
||||||
|
) -> None:
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jwt as pyjwt
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
import PyJWT as pyjwt # type: ignore
|
||||||
|
|
||||||
|
secret = os.environ.get("CUSTOMER_JWT_SECRET", "changeme_long_random_string")
|
||||||
|
domain = os.environ.get("DOMAIN", "performancewest.net")
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
token = pyjwt.encode(
|
||||||
|
{
|
||||||
|
"order_id": order_number,
|
||||||
|
"order_type": document_type,
|
||||||
|
"email": customer_email,
|
||||||
|
"exp": datetime.now(timezone.utc) + timedelta(days=expires_days),
|
||||||
|
},
|
||||||
|
secret,
|
||||||
|
algorithm="HS256",
|
||||||
|
)
|
||||||
|
sign_url = f"https://{domain}/portal/esign?token={token}"
|
||||||
|
|
||||||
|
dot_line = f" (DOT# {dot_number})" if dot_number else ""
|
||||||
|
body = (
|
||||||
|
f"Hi,\n\n"
|
||||||
|
f"Your {document_title} for {entity_name}{dot_line} has been prepared and "
|
||||||
|
f"is ready for your signature.\n\n"
|
||||||
|
f"Federal law requires your certification before we can submit this filing. "
|
||||||
|
f"Please review and sign here:\n{sign_url}\n\n"
|
||||||
|
f"This link expires in {expires_days} days.\n\n"
|
||||||
|
f"Once you sign, we file with the appropriate agency and send you "
|
||||||
|
f"confirmation.\n\n"
|
||||||
|
f"Order: {order_number}\n"
|
||||||
|
f"Questions? Call (888) 411-0383.\n\n"
|
||||||
|
f"Performance West Inc.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = MIMEText(body)
|
||||||
|
msg["Subject"] = f"Action Required: Sign Your {document_title} — {entity_name}"
|
||||||
|
msg["From"] = "noreply@performancewest.net"
|
||||||
|
msg["To"] = customer_email
|
||||||
|
|
||||||
|
with smtplib.SMTP("localhost", 25, timeout=30) as s:
|
||||||
|
s.sendmail(msg["From"], [customer_email], msg.as_string())
|
||||||
|
|
@ -74,10 +74,18 @@ class MCS150UpdateHandler:
|
||||||
if isinstance(intake, str):
|
if isinstance(intake, str):
|
||||||
intake = json.loads(intake)
|
intake = json.loads(intake)
|
||||||
|
|
||||||
|
slug = order_data.get("service_slug", self.SERVICE_SLUG)
|
||||||
dot_number = intake.get("dot_number", "")
|
dot_number = intake.get("dot_number", "")
|
||||||
entity_name = intake.get("entity_name", order_data.get("customer_name", ""))
|
entity_name = intake.get("entity_name", order_data.get("customer_name", ""))
|
||||||
customer_email = order_data.get("customer_email", "")
|
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
|
# Validate required fields
|
||||||
if not dot_number:
|
if not dot_number:
|
||||||
LOG.error("[%s] Missing DOT number in intake data", order_number)
|
LOG.error("[%s] Missing DOT number in intake data", order_number)
|
||||||
|
|
@ -140,50 +148,27 @@ class MCS150UpdateHandler:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOG.warning("[%s] Could not retrieve photo ID: %s", order_number, exc)
|
LOG.warning("[%s] Could not retrieve photo ID: %s", order_number, exc)
|
||||||
|
|
||||||
# Step 4: Create e-sign record for perjury declaration
|
# Step 4: SIGNATURE GATE. If this form needs the client's signed
|
||||||
# Customer must sign before we submit to FMCSA
|
# certification and they haven't signed yet, request the signature and
|
||||||
if pdf_path and minio_path:
|
# STOP before filing. We never submit a perjury certification to FMCSA
|
||||||
try:
|
# until the client has actually signed it.
|
||||||
import psycopg2
|
if needs_signature and not client_approved:
|
||||||
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
request_dot_esign(
|
||||||
cur = conn.cursor()
|
order_number=order_number,
|
||||||
cur.execute("""
|
slug=slug,
|
||||||
INSERT INTO esign_records (
|
entity_name=entity_name,
|
||||||
order_number, document_type, document_title, entity_name,
|
customer_email=customer_email,
|
||||||
document_minio_key, document_metadata,
|
dot_number=dot_number,
|
||||||
requires_perjury, status, expires_at
|
document_minio_key=minio_path or "",
|
||||||
) VALUES (%s, %s, %s, %s, %s, %s, TRUE, 'pending', NOW() + interval '7 days')
|
)
|
||||||
ON CONFLICT (order_number, document_type)
|
LOG.info("[%s] Awaiting client signature before filing %s — not submitting yet",
|
||||||
WHERE status IN ('pending', 'signed') DO NOTHING
|
order_number, slug)
|
||||||
""", (
|
self._create_pending_signature_todo(
|
||||||
order_number,
|
order_number, entity_name, dot_number, slug, minio_path, customer_email)
|
||||||
"mcs150",
|
return [minio_path] if minio_path else []
|
||||||
"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)
|
|
||||||
|
|
||||||
# Send e-sign link to customer
|
# Past this point: either no signature is required for this service, or
|
||||||
self._send_esign_email(order_number, entity_name, dot_number, customer_email)
|
# the client has signed (re-dispatched with client_approved=True).
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Step 5: Submit electronically (3x web → fax fallback)
|
# Step 5: Submit electronically (3x web → fax fallback)
|
||||||
# GUARD: Skip actual submission in dev/test environments
|
# GUARD: Skip actual submission in dev/test environments
|
||||||
|
|
@ -320,48 +305,37 @@ class MCS150UpdateHandler:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return f"Could not check: {exc}"
|
return f"Could not check: {exc}"
|
||||||
|
|
||||||
def _send_esign_email(self, order_number, entity_name, dot_number, customer_email):
|
def _create_pending_signature_todo(self, order_number, entity_name, dot_number,
|
||||||
"""Send e-sign link for MCS-150 perjury declaration."""
|
slug, minio_path, customer_email):
|
||||||
if not customer_email:
|
"""Low-priority admin todo noting we're waiting on the client's signature."""
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
import smtplib
|
import psycopg2
|
||||||
import jwt
|
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
||||||
from email.mime.text import MIMEText
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
secret = os.environ.get("JWT_SECRET", os.environ.get("ADMIN_JWT_SECRET", ""))
|
"""
|
||||||
token = jwt.encode(
|
INSERT INTO admin_todos (
|
||||||
{"order_id": order_number, "order_type": "mcs150", "email": customer_email},
|
title, category, priority, order_number, service_slug,
|
||||||
secret, algorithm="HS256",
|
description, data, status
|
||||||
)
|
) VALUES (%s, %s, %s, %s, %s, %s, %s, 'pending')
|
||||||
domain = os.environ.get("DOMAIN", "performancewest.net")
|
""",
|
||||||
esign_url = f"https://{domain}/portal/esign?token={token}"
|
(
|
||||||
|
f"Awaiting client signature — {entity_name} (DOT {dot_number})",
|
||||||
body = (
|
"filing", "low", order_number, slug,
|
||||||
f"Hi,\n\n"
|
f"{slug} for {entity_name} (DOT {dot_number}).\n"
|
||||||
f"Your MCS-150 Biennial Update for {entity_name} (DOT# {dot_number}) "
|
f"Status: AWAITING CLIENT SIGNATURE before filing.\n"
|
||||||
f"has been prepared and is ready for your signature.\n\n"
|
f"Signing link emailed to {customer_email}.\n"
|
||||||
f"Federal law requires your certification under penalty of perjury "
|
f"PDF: {minio_path or 'not generated'}\n"
|
||||||
f"before we can submit this form to FMCSA.\n\n"
|
f"Filing auto-resumes once the client signs.",
|
||||||
f"Please review and sign here:\n{esign_url}\n\n"
|
json.dumps({"order_number": order_number, "dot_number": dot_number,
|
||||||
f"This link expires in 7 days.\n\n"
|
"entity_name": entity_name, "awaiting_signature": True}),
|
||||||
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"
|
conn.commit()
|
||||||
f"Performance West Inc.\n"
|
conn.close()
|
||||||
)
|
LOG.info("[%s] Pending-signature todo created", order_number)
|
||||||
|
|
||||||
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)
|
|
||||||
except Exception as exc:
|
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):
|
def _send_status_email(self, order_number, entity_name, dot_number, customer_email):
|
||||||
"""Send client an email that we're working on their update."""
|
"""Send client an email that we're working on their update."""
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Step 3 — Confirm & Submit</h2>
|
<h2>Step 3 — Confirm & Submit</h2>
|
||||||
|
|
||||||
|
<div id="attestation-box" class="perjury hidden" style="font-style:normal;color:#1f2937;background:#eff6ff;border-color:#bfdbfe"></div>
|
||||||
|
|
||||||
<div id="perjury-box" class="perjury hidden">
|
<div id="perjury-box" class="perjury hidden">
|
||||||
I declare under penalty of perjury under the laws of the United States of America that the foregoing is true and correct. Executed on <span id="perjury-date"></span>.
|
I declare under penalty of perjury under the laws of the United States of America that the foregoing is true and correct. Executed on <span id="perjury-date"></span>.
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,6 +176,16 @@ body{font-family:'Inter',system-ui,sans-serif;color:#1f2937;background:#f1f5f9;l
|
||||||
document.getElementById("entity-confirm").textContent = data.entity_name;
|
document.getElementById("entity-confirm").textContent = data.entity_name;
|
||||||
document.getElementById("perjury-date").textContent = new Date().toLocaleDateString("en-US", {year:"numeric",month:"long",day:"numeric"});
|
document.getElementById("perjury-date").textContent = new Date().toLocaleDateString("en-US", {year:"numeric",month:"long",day:"numeric"});
|
||||||
|
|
||||||
|
// Show the specific certification/attestation language for this document
|
||||||
|
// (e.g. the dissolution "no outstanding lawsuits, liens, or judgments"
|
||||||
|
// attestation, or the OP-1 / MCS-150 certification).
|
||||||
|
var attestText = data.metadata && data.metadata.perjury_text;
|
||||||
|
if (attestText) {
|
||||||
|
var ab = document.getElementById("attestation-box");
|
||||||
|
ab.textContent = attestText;
|
||||||
|
ab.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
if (data.requires_perjury) {
|
if (data.requires_perjury) {
|
||||||
document.getElementById("perjury-box").classList.remove("hidden");
|
document.getElementById("perjury-box").classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue