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:
parent
345979ed00
commit
7ed06780bb
9 changed files with 1322 additions and 5 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue