new-site/scripts/workers/services/esign_stamp.py
justin 7ed06780bb 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)
2026-06-02 16:44:19 -05:00

122 lines
4.6 KiB
Python

"""Stamp the captured signature onto the form PDF stored in MinIO.
Glue between the e-sign record (signature image + recorded anchor coordinates)
and the actual form document. After a client signs, call
:func:`stamp_esign_document` to:
1. read the signature, type, and signature_anchors from esign_records;
2. download the unsigned form PDF (document_minio_key) from MinIO;
3. stamp the signature onto the recorded signature line(s);
4. upload the flattened signed PDF and store signed_document_minio_key.
Safe to call more than once — it is idempotent on signed_document_minio_key.
"""
from __future__ import annotations
import json
import logging
import os
import tempfile
LOG = logging.getLogger("workers.services.esign_stamp")
def stamp_esign_document(esign_record_id: int) -> str | None:
"""Stamp the signature onto the form for one esign_records row.
Returns the signed document's MinIO key, or None if nothing was stamped
(no document, no anchors, or already stamped).
"""
try:
import psycopg2
except ImportError: # pragma: no cover
LOG.error("psycopg2 unavailable — cannot stamp signature")
return None
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
try:
with conn.cursor() as cur:
cur.execute(
"""SELECT order_number, document_type, signature_type,
signature_data, document_minio_key, signature_anchors,
signed_document_minio_key
FROM esign_records WHERE id = %s""",
(esign_record_id,),
)
row = cur.fetchone()
if not row:
LOG.warning("[esign_stamp] No esign record %s", esign_record_id)
return None
(order_number, document_type, signature_type, signature_data,
document_key, anchors_raw, already_signed_key) = row
if already_signed_key:
LOG.info("[esign_stamp] %s already stamped (%s)", order_number, already_signed_key)
return already_signed_key
if not document_key:
LOG.info("[esign_stamp] %s has no form PDF to stamp", order_number)
return None
anchors = anchors_raw if isinstance(anchors_raw, list) else (
json.loads(anchors_raw) if anchors_raw else []
)
if not anchors:
LOG.info("[esign_stamp] %s has no signature anchors — skipping stamp", order_number)
return None
if not signature_type or not signature_data:
LOG.warning("[esign_stamp] %s missing signature data", order_number)
return None
# Download the unsigned form PDF.
try:
from scripts.document_gen.minio_client import MinioStorage
except ImportError: # pragma: no cover - alt sys.path layouts
from document_gen.minio_client import MinioStorage # type: ignore
try:
from .signature_stamper import stamp_signature
except ImportError: # pragma: no cover
from signature_stamper import stamp_signature # type: ignore
storage = MinioStorage()
with tempfile.TemporaryDirectory() as tmp:
local_in = os.path.join(tmp, "form.pdf")
storage.download(document_key, local_in)
with open(local_in, "rb") as f:
pdf_bytes = f.read()
signed_bytes = stamp_signature(
pdf_bytes,
anchors,
signature_type=signature_type,
signature_data=signature_data,
signer_field="signer",
)
local_out = os.path.join(tmp, "form_signed.pdf")
with open(local_out, "wb") as f:
f.write(signed_bytes)
# Signed key sits next to the original with a _signed suffix.
if document_key.endswith(".pdf"):
signed_key = document_key[:-4] + "_signed.pdf"
else:
signed_key = document_key + "_signed.pdf"
storage.upload(local_out, signed_key, content_type="application/pdf")
with conn.cursor() as cur:
cur.execute(
"UPDATE esign_records SET signed_document_minio_key = %s, updated_at = NOW() WHERE id = %s",
(signed_key, esign_record_id),
)
conn.commit()
LOG.info("[esign_stamp] %s signature stamped onto form -> %s", order_number, signed_key)
return signed_key
except Exception as exc:
conn.rollback()
LOG.error("[esign_stamp] Failed for record %s: %s", esign_record_id, exc)
return None
finally:
conn.close()