trucking: stamp e-signature exactly on form signature lines + state authorization gate

Capture-to-form signature placement so the customer's drawn or typed
signature lands right on the signature rule of the actual form, not in a
sidecar page.

- migration 085: esign_records.signature_anchors (JSONB exact PDF coords,
  lower-left origin, points) + signed_document_minio_key
- signature_stamper.py: signature_box() anchors; anchors_from_acroform()
  pulls the signature field /Rect from a real AcroForm (e.g. MCS-150
  certifySignature); stamp_signature() overlays PNG (auto-trimmed so ink
  rests on the rule) or typed name, scaled to actual page size
- state_trucking_authorization.py: renders the Limited Authorization to
  File PDF and returns (pdf_bytes, anchors)
- esign_stamp.py: stamp_esign_document() downloads unsigned PDF, stamps,
  uploads _signed.pdf, sets signed_document_minio_key (idempotent)
- dot_esign.py: extract certifySignature anchor for MCS-150/closeout forms
  so the federal perjury cert is signed on the line
- state_trucking.py: authorization gate — first run emails signing link
  and PAUSES; resumes with client_approved after signing
- job_server handle_esign_completed: stamp then re-dispatch
- tests: test_signature_placement.py (custom form), and
  test_mcs150_signature_placement.py (official AcroForm) both assert the
  signature lands inside the recorded signature box (verified visually)
This commit is contained in:
justin 2026-06-02 16:44:19 -05:00
parent 345979ed00
commit 7ed06780bb
9 changed files with 1322 additions and 5 deletions

View file

@ -233,6 +233,46 @@ class StateTruckingHandler:
base_state = intake.get("base_state", intake.get("phy_state", ""))
operating_states = intake.get("operating_states", [])
# ── Authorization gate ───────────────────────────────────────────
# Every state portal filing legally requires the customer's signed
# "Limited Authorization to File State Motor Carrier Compliance
# Documents" before we touch a state portal/tax system. On the first
# run we generate that authorization, email the signing link, and
# PAUSE. The pipeline resumes here with client_approved=True once the
# customer signs (handle_esign_completed re-dispatches us).
client_approved = bool(order_data.get("client_approved"))
signed_auth_key = None
if not client_approved:
requested = self._request_authorization(
order_number=order_number,
service_slug=service_slug,
service_name=service_name,
entity_name=entity_name,
customer_email=customer_email,
dot_number=dot_number,
mc_number=str(intake.get("mc_number", "")),
fein_last4=str(intake.get("fein_last4", "")),
base_state=base_state,
operating_states=operating_states,
signer_name=str(intake.get("signer_name", order_data.get("customer_name", ""))),
signer_title=str(intake.get("signer_title", "")),
)
if requested:
LOG.info(
"[%s] Authorization requested — pipeline PAUSED pending signature",
order_number,
)
return []
# If we could not request a signature (e.g. no email), fall through
# and create the admin todo so ops can chase the authorization.
LOG.warning(
"[%s] Could not request authorization e-sign — proceeding to admin todo",
order_number,
)
else:
signed_auth_key = self._signed_authorization_key(order_number)
LOG.info("[%s] Authorization signed (%s) — proceeding to filing", order_number, signed_auth_key)
# Slug-specific intake fields collected by StateTruckingIntakeStep.
intake_summary = self._summarize_intake(service_slug, intake)
@ -263,6 +303,7 @@ class StateTruckingHandler:
"intake_summary": intake_summary,
"state_requirements": state_reqs,
"steps": steps,
"signed_authorization_minio_key": signed_auth_key,
}
# Render the slug-specific intake fields into the description.
@ -431,10 +472,236 @@ class StateTruckingHandler:
return {"agency": reqs[keys[0]], "url": reqs.get(keys[1], "")}
return None
# ── Authorization (signed "Limited Authorization to File") ──────────
# document_type used in esign_records for the state-trucking authorization.
AUTH_DOCUMENT_TYPE = "state-trucking-authorization"
def _request_authorization(
self,
*,
order_number: str,
service_slug: str,
service_name: str,
entity_name: str,
customer_email: str,
dot_number: str = "",
mc_number: str = "",
fein_last4: str = "",
base_state: str = "",
operating_states: list | None = None,
signer_name: str = "",
signer_title: str = "",
) -> bool:
"""Generate + upload the authorization PDF and email the signing link.
Returns True if a signing request was created (pipeline should pause),
False if it could not be requested (e.g. no customer email) so the
caller can fall back to an admin todo.
"""
if not customer_email:
return False
# If a record already exists (pending or signed), don't duplicate.
existing = self._authorization_status(order_number)
if existing == "signed":
return False # already signed — proceed to filing
# (pending records are upserted/refreshed below)
try:
from .state_trucking_authorization import build_state_trucking_authorization
from .signature_stamper import signature_box # noqa: F401 (ensures module import)
except Exception as exc:
LOG.error("[%s] Authorization generator unavailable: %s", order_number, exc)
return False
try:
pdf_bytes, anchors = build_state_trucking_authorization(
order_number=order_number,
entity_name=entity_name,
service_name=service_name,
dot_number=dot_number,
mc_number=mc_number,
fein_last4=fein_last4,
base_state=base_state,
operating_states=operating_states or [],
signer_name=signer_name,
signer_title=signer_title,
)
except Exception as exc:
LOG.error("[%s] Failed to render authorization PDF: %s", order_number, exc)
return False
# Upload the unsigned authorization to MinIO.
document_key = f"compliance/{order_number}/state_trucking_authorization.pdf"
try:
import tempfile
try:
from scripts.document_gen.minio_client import MinioStorage
except ImportError:
from document_gen.minio_client import MinioStorage # type: ignore
storage = MinioStorage()
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=True) as tf:
tf.write(pdf_bytes)
tf.flush()
storage.upload(tf.name, document_key, content_type="application/pdf")
except Exception as exc:
LOG.error("[%s] Failed to upload authorization PDF: %s", order_number, exc)
return False
# Create the esign record (with the signature anchors so the stamper can
# place the signature on the line) and email the signing link.
try:
import json
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
try:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO esign_records (
order_number, document_type, document_title, entity_name,
document_minio_key, document_metadata, signature_anchors,
requires_perjury, status, expires_at
) VALUES (%s, %s, %s, %s, %s, %s, %s, FALSE, 'pending',
NOW() + INTERVAL '14 days')
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,
signature_anchors = EXCLUDED.signature_anchors,
expires_at = EXCLUDED.expires_at,
updated_at = NOW()
""",
(
order_number,
self.AUTH_DOCUMENT_TYPE,
f"Authorization to File: {service_name}",
entity_name,
document_key,
json.dumps({"service_slug": service_slug, "dot_number": dot_number}),
json.dumps(anchors),
),
)
conn.commit()
finally:
conn.close()
except Exception as exc:
LOG.error("[%s] Failed to create authorization esign record: %s", order_number, exc)
return False
# Email the signing link (JWT signed with CUSTOMER_JWT_SECRET).
try:
self._send_authorization_email(
order_number=order_number,
service_name=service_name,
entity_name=entity_name,
customer_email=customer_email,
)
except Exception as exc:
LOG.warning("[%s] Authorization email failed (record exists): %s", order_number, exc)
return True
def _authorization_status(self, order_number: str) -> str | None:
"""Return the current authorization esign status: 'signed', 'pending', or None."""
try:
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
try:
with conn.cursor() as cur:
cur.execute(
"""SELECT status FROM esign_records
WHERE order_number = %s AND document_type = %s
AND status IN ('pending', 'signed')
ORDER BY created_at DESC LIMIT 1""",
(order_number, self.AUTH_DOCUMENT_TYPE),
)
row = cur.fetchone()
return row[0] if row else None
finally:
conn.close()
except Exception:
return None
def _signed_authorization_key(self, order_number: str) -> str | None:
"""Return the MinIO key of the stamped, signed authorization PDF if available."""
try:
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
try:
with conn.cursor() as cur:
cur.execute(
"""SELECT signed_document_minio_key, document_minio_key
FROM esign_records
WHERE order_number = %s AND document_type = %s
AND status = 'signed'
ORDER BY signed_at DESC NULLS LAST LIMIT 1""",
(order_number, self.AUTH_DOCUMENT_TYPE),
)
row = cur.fetchone()
if not row:
return None
return row[0] or row[1]
finally:
conn.close()
except Exception:
return None
def _send_authorization_email(
self, *, order_number, service_name, entity_name, customer_email
):
"""Email the customer a link to review and sign the authorization."""
import os as _os
import smtplib
from datetime import datetime, timedelta, timezone
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")
token = pyjwt.encode(
{
"order_id": order_number,
"order_type": self.AUTH_DOCUMENT_TYPE,
"email": customer_email,
"exp": datetime.now(timezone.utc) + timedelta(days=14),
},
secret,
algorithm="HS256",
)
sign_url = f"https://{domain}/portal/esign?token={token}"
body = (
f"Hi,\n\n"
f"To complete your {service_name} order for {entity_name}, we need your "
f"signed authorization before we can file with the state on your behalf.\n\n"
f"Please review and sign here:\n{sign_url}\n\n"
f"This authorization lets Performance West prepare and submit your state "
f"motor carrier filing, communicate with the agency, and remit government "
f"fees you provide. Government fees, taxes, card processing fees, decals, "
f"permits, bonds, and insurance filing costs are pass-through charges and "
f"may be billed separately if not known at checkout.\n\n"
f"This link expires in 14 days.\n\n"
f"Order: {order_number}\n"
f"Questions? Call (888) 411-0383.\n\n"
f"Performance West Inc.\nDOT / State Motor Carrier Compliance\n"
)
msg = MIMEText(body)
msg["Subject"] = f"Action Required: Sign Your Authorization — {service_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())
def _send_status_email(self, order_number, service_name, entity_name, dot_number, customer_email):
"""Send client a status email."""
if not customer_email:
return
try:
import smtplib
from email.mime.text import MIMEText