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

@ -104,6 +104,47 @@ def requires_signature(slug: str) -> bool:
return slug in DOT_SIGNING
# AcroForm signature field name -> logical anchor field, per document_type.
# The stamper grows the box upward so the signature rests on the official rule.
DOC_SIG_FIELDS: dict[str, dict[str, str]] = {
"mcs150": {"certifySignature": "signer"},
"carrier-closeout": {"certifySignature": "signer"},
}
def _extract_acroform_anchors(document_minio_key: str, document_type: str) -> list | None:
"""Download the unsigned form from MinIO and return signature anchors.
Returns a list of anchor dicts (placed on the official signature line) for
document types backed by a real AcroForm PDF, or None if not applicable / on
any failure (the signing flow still works without anchors, falling back to a
typed-name overlay only if anchors are present).
"""
field_map = DOC_SIG_FIELDS.get(document_type)
if not field_map or not document_minio_key:
return None
try:
import tempfile
from .signature_stamper import anchors_from_acroform
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:
storage.download(document_minio_key, tf.name)
tf.seek(0)
pdf_bytes = open(tf.name, "rb").read()
anchors = anchors_from_acroform(pdf_bytes, field_map)
return anchors or None
except Exception as exc:
LOG.warning("Could not extract signature anchors from %s: %s", document_minio_key, exc)
return None
def request_dot_esign(
order_number: str,
slug: str,
@ -140,6 +181,10 @@ def request_dot_esign(
if extra_metadata:
metadata.update(extra_metadata)
# Extract the exact signature-line coordinates from the unsigned form so the
# client's signature can be stamped right on the rule after they sign.
anchors = _extract_acroform_anchors(document_minio_key, document_type)
# 1. Upsert the pending record
esign_id = None
try:
@ -151,9 +196,9 @@ def request_dot_esign(
"""
INSERT INTO esign_records (
order_number, document_type, document_title, entity_name,
document_minio_key, document_metadata,
document_minio_key, document_metadata, signature_anchors,
requires_perjury, status, expires_at
) VALUES (%s, %s, %s, %s, %s, %s, TRUE, 'pending',
) VALUES (%s, %s, %s, %s, %s, %s, %s, TRUE, 'pending',
NOW() + (%s || ' days')::interval)
ON CONFLICT (order_number, document_type)
WHERE status IN ('pending', 'signed')
@ -162,13 +207,15 @@ def request_dot_esign(
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()
RETURNING id
""",
(
order_number, document_type, document_title, entity_name,
document_minio_key, json.dumps(metadata), str(expires_days),
document_minio_key, json.dumps(metadata),
json.dumps(anchors) if anchors else None, str(expires_days),
),
)
row = cur.fetchone()