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
278
scripts/workers/services/state_trucking_authorization.py
Normal file
278
scripts/workers/services/state_trucking_authorization.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
"""Generate the state-trucking "Limited Authorization to File" PDF.
|
||||
|
||||
This is the signed authorization the customer must provide before Performance
|
||||
West can file state motor-carrier compliance documents on their behalf (NY HUT,
|
||||
CT HUF, OR Weight-Mile, CA MCP/CARB, KY KYU, NM WDT, IRP/IFTA, etc.). See
|
||||
docs/trucking-state-authorization-plan.md.
|
||||
|
||||
The generator draws an explicit signature line and records its exact PDF
|
||||
coordinates via :func:`signature_box`, so the e-signature stamper can place the
|
||||
client's signature precisely on that line (see signature_stamper.py).
|
||||
|
||||
Returns ``(pdf_bytes, anchors)`` where ``anchors`` is the list to persist in
|
||||
``esign_records.signature_anchors``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
try: # normal import when loaded as part of the services package
|
||||
from .signature_stamper import signature_box
|
||||
except ImportError: # pragma: no cover - standalone/CLI use
|
||||
from signature_stamper import signature_box # type: ignore
|
||||
|
||||
LOG = logging.getLogger("workers.services.state_trucking_authorization")
|
||||
|
||||
NAVY = "#0b1f3a"
|
||||
SLATE = "#475569"
|
||||
RULE = "#94a3b8"
|
||||
|
||||
_AUTH_BODY = (
|
||||
"By signing below, the Customer named above authorizes Performance West Inc. "
|
||||
"(\"Performance West\") to act as its limited agent to:"
|
||||
)
|
||||
|
||||
_AUTH_GRANTS = [
|
||||
"prepare, review, submit, and amend the state motor carrier compliance "
|
||||
"filing(s) identified in this authorization;",
|
||||
"access state agency portals or tax systems where representative, "
|
||||
"practitioner, bulk-filer, or delegate access is available;",
|
||||
"communicate with state agencies about the filing and receive filing status, "
|
||||
"notices, confirmations, decals, credentials, and deficiency messages;",
|
||||
"coordinate with the Customer's insurer or insurance agent for any required "
|
||||
"insurance filings;",
|
||||
"remit government fees, taxes, card processing fees, decals, permits, bonds, "
|
||||
"and pass-through charges when collected from the Customer;",
|
||||
"retain a copy of this signed authorization for agency audit or request "
|
||||
"purposes.",
|
||||
]
|
||||
|
||||
_AUTH_ACK = [
|
||||
"The Customer remains responsible for the accuracy and completeness of the "
|
||||
"information it provides.",
|
||||
"Government fees, taxes, card processing fees, bonds/security, decals, "
|
||||
"permits, and insurance filing costs are pass-through charges and may be "
|
||||
"billed separately if not known at the time of order.",
|
||||
"Some state filings require the Customer to grant portal access/delegation or "
|
||||
"approve account changes; Performance West will request this where needed.",
|
||||
"This authorization is limited to the filing(s) listed and may be revoked in "
|
||||
"writing, except as to filings already submitted.",
|
||||
]
|
||||
|
||||
|
||||
def build_state_trucking_authorization(
|
||||
*,
|
||||
order_number: str,
|
||||
entity_name: str,
|
||||
service_name: str,
|
||||
dot_number: str = "",
|
||||
mc_number: str = "",
|
||||
fein_last4: str = "",
|
||||
base_state: str = "",
|
||||
operating_states: list[str] | None = None,
|
||||
signer_name: str = "",
|
||||
signer_title: str = "",
|
||||
) -> tuple[bytes, list[dict[str, Any]]]:
|
||||
"""Render the authorization PDF and return (pdf_bytes, signature_anchors)."""
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
buf = io.BytesIO()
|
||||
page_w, page_h = letter # 612 x 792
|
||||
c = canvas.Canvas(buf, pagesize=letter)
|
||||
|
||||
left = 0.85 * inch
|
||||
right = page_w - 0.85 * inch
|
||||
width = right - left
|
||||
|
||||
def wrap(text: str, font: str, size: float, max_w: float) -> list[str]:
|
||||
c.setFont(font, size)
|
||||
words = text.split()
|
||||
lines: list[str] = []
|
||||
cur = ""
|
||||
for w in words:
|
||||
trial = (cur + " " + w).strip()
|
||||
if c.stringWidth(trial, font, size) <= max_w:
|
||||
cur = trial
|
||||
else:
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
cur = w
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
return lines
|
||||
|
||||
y = page_h - 0.9 * inch
|
||||
|
||||
# Header
|
||||
c.setFillColor(HexColor(NAVY))
|
||||
c.setFont("Helvetica-Bold", 16)
|
||||
c.drawString(left, y, "Performance West Inc.")
|
||||
y -= 18
|
||||
c.setFont("Helvetica", 9.5)
|
||||
c.setFillColor(HexColor(SLATE))
|
||||
c.drawString(left, y, "DOT / State Motor Carrier Compliance Division | (888) 411-0383 | performancewest.net")
|
||||
y -= 8
|
||||
c.setStrokeColor(HexColor(RULE))
|
||||
c.line(left, y, right, y)
|
||||
y -= 26
|
||||
|
||||
c.setFillColor(HexColor(NAVY))
|
||||
c.setFont("Helvetica-Bold", 13.5)
|
||||
c.drawString(left, y, "Limited Authorization to File State Motor Carrier Compliance Documents")
|
||||
y -= 22
|
||||
|
||||
# Customer / order block
|
||||
def field_row(label: str, value: str) -> None:
|
||||
nonlocal y
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.setFillColor(HexColor(NAVY))
|
||||
c.drawString(left, y, label)
|
||||
c.setFont("Helvetica", 10)
|
||||
c.setFillColor(HexColor("#1f2937"))
|
||||
c.drawString(left + 1.6 * inch, y, value or "—")
|
||||
y -= 15
|
||||
|
||||
states = ", ".join(operating_states or []) if operating_states else (base_state or "—")
|
||||
field_row("Customer (Carrier):", entity_name)
|
||||
field_row("USDOT #:", dot_number)
|
||||
if mc_number:
|
||||
field_row("MC/MX/FF #:", mc_number)
|
||||
if fein_last4:
|
||||
field_row("FEIN (last 4):", fein_last4)
|
||||
field_row("Covered filing:", service_name)
|
||||
field_row("Covered state(s):", states)
|
||||
field_row("Order #:", order_number)
|
||||
y -= 8
|
||||
|
||||
# Body intro
|
||||
c.setFillColor(HexColor("#1f2937"))
|
||||
for ln in wrap(_AUTH_BODY, "Helvetica", 10, width):
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(left, y, ln)
|
||||
y -= 14
|
||||
y -= 4
|
||||
|
||||
# Numbered grants
|
||||
for i, grant in enumerate(_AUTH_GRANTS, 1):
|
||||
bullet = f"{i}."
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(left, y, bullet)
|
||||
lines = wrap(grant, "Helvetica", 10, width - 0.3 * inch)
|
||||
for j, ln in enumerate(lines):
|
||||
c.setFont("Helvetica", 10)
|
||||
c.drawString(left + 0.3 * inch, y, ln)
|
||||
y -= 13
|
||||
y -= 3
|
||||
|
||||
y -= 6
|
||||
c.setFont("Helvetica-Bold", 10.5)
|
||||
c.setFillColor(HexColor(NAVY))
|
||||
c.drawString(left, y, "Acknowledgements")
|
||||
y -= 16
|
||||
for ack in _AUTH_ACK:
|
||||
c.setFillColor(HexColor("#475569"))
|
||||
c.setFont("Helvetica", 9.5)
|
||||
c.drawString(left, y, "\u2022")
|
||||
for j, ln in enumerate(wrap(ack, "Helvetica", 9.5, width - 0.25 * inch)):
|
||||
c.drawString(left + 0.25 * inch, y, ln)
|
||||
y -= 12
|
||||
y -= 2
|
||||
|
||||
# ── Signature block ──────────────────────────────────────────────
|
||||
# Reserve a fixed band near the bottom so the signature line position is
|
||||
# deterministic. The signature box sits ON TOP of the rule.
|
||||
sig_rule_y = 1.55 * inch
|
||||
sig_box_h = 42.0
|
||||
sig_box_w = 3.1 * inch
|
||||
sig_x = left
|
||||
|
||||
c.setFillColor(HexColor(NAVY))
|
||||
c.setFont("Helvetica-Bold", 10.5)
|
||||
c.drawString(left, sig_rule_y + sig_box_h + 14, "Authorized Signature")
|
||||
|
||||
# The signature rule
|
||||
c.setStrokeColor(HexColor("#334155"))
|
||||
c.setLineWidth(1)
|
||||
c.line(sig_x, sig_rule_y, sig_x + sig_box_w, sig_rule_y)
|
||||
|
||||
# Date rule to the right
|
||||
date_x = sig_x + sig_box_w + 0.5 * inch
|
||||
date_w = right - date_x
|
||||
c.line(date_x, sig_rule_y, right, sig_rule_y)
|
||||
|
||||
c.setFont("Helvetica", 8.5)
|
||||
c.setFillColor(HexColor(SLATE))
|
||||
c.drawString(sig_x, sig_rule_y - 11, "Signature")
|
||||
c.drawString(date_x, sig_rule_y - 11, "Date")
|
||||
|
||||
# Printed name / title beneath
|
||||
ny = sig_rule_y - 30
|
||||
c.setFont("Helvetica", 9.5)
|
||||
c.setFillColor(HexColor("#1f2937"))
|
||||
c.drawString(sig_x, ny, f"Printed name: {signer_name or '________________________'}")
|
||||
c.drawString(date_x, ny, f"Title: {signer_title or '____________'}")
|
||||
|
||||
# Footer note
|
||||
c.setFont("Helvetica-Oblique", 7.5)
|
||||
c.setFillColor(HexColor("#94a3b8"))
|
||||
c.drawString(
|
||||
left,
|
||||
0.75 * inch,
|
||||
f"Generated {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')} | "
|
||||
f"Order {order_number} | Performance West Inc.",
|
||||
)
|
||||
|
||||
c.save()
|
||||
buf.seek(0)
|
||||
pdf_bytes = buf.getvalue()
|
||||
|
||||
# Record the exact signature box so the stamper lands the signature on the
|
||||
# rule. The box bottom == the rule's y, height extends upward.
|
||||
anchors = [
|
||||
signature_box(
|
||||
field="signer",
|
||||
page=0,
|
||||
x=sig_x + 4, # small inset from the line's left end
|
||||
y=sig_rule_y + 1, # rest the signature just on top of the rule
|
||||
w=sig_box_w - 8,
|
||||
h=sig_box_h,
|
||||
page_w=page_w,
|
||||
page_h=page_h,
|
||||
),
|
||||
signature_box(
|
||||
field="date",
|
||||
page=0,
|
||||
x=date_x + 4,
|
||||
y=sig_rule_y + 1,
|
||||
w=date_w - 8,
|
||||
h=20.0,
|
||||
page_w=page_w,
|
||||
page_h=page_h,
|
||||
),
|
||||
]
|
||||
return pdf_bytes, anchors
|
||||
|
||||
|
||||
if __name__ == "__main__": # quick local render for visual verification
|
||||
pdf, anchors = build_state_trucking_authorization(
|
||||
order_number="CO-TEST1234",
|
||||
entity_name="Acme Freight LLC",
|
||||
service_name="New York Highway Use Tax (HUT) Registration",
|
||||
dot_number="3456789",
|
||||
mc_number="MC-987654",
|
||||
fein_last4="1234",
|
||||
base_state="NY",
|
||||
operating_states=["NY", "PA", "OH"],
|
||||
signer_name="John Smith",
|
||||
signer_title="Owner",
|
||||
)
|
||||
with open("/tmp/state_trucking_authorization.pdf", "wb") as f:
|
||||
f.write(pdf)
|
||||
print("wrote /tmp/state_trucking_authorization.pdf")
|
||||
print("anchors:", anchors)
|
||||
Loading…
Add table
Add a link
Reference in a new issue