"""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()