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
122
scripts/workers/services/esign_stamp.py
Normal file
122
scripts/workers/services/esign_stamp.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
"""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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue