"""What a signing link shows and does, on the composition's PUBLIC app (erp_platform.public; no pool, no MCP, no PDF
engine — it serves the frozen bytes). The handlers only render and call this brick's public use cases (signing.py)
with the door's public identity, scoped to one signer of one contract.
SIGN link (READ + SIGN) GET: the frozen version's words, its sha256, the consent text and how long the copy is
kept — OPENED is recorded with IP and user agent. POST action=download: the frozen PDF;
action=sign | decline | changes: the act, claimed once (link.use inside the signing
transaction when it shares our engine). No script is needed to sign.
READ link (signed copy) GET: the executed copy's bytes, after its evidence verifies.
A used, revoked or expired link gets the door's one dead-link page — the same for every reason.
"""
from erp_platform import public
from erp_platform.bricks import Refused
from brick_contracts.application import signing
from brick_contracts.application.use_cases import CT
esc = public.esc
def _page(v: dict, notice: str = "") -> public.Page:
text = "".join(f"<p style='white-space:pre-wrap;margin:.2rem 0'>{esc(line) or ' '}</p>"
for line in (v.get("text") or "").split("\n"))
turn = ("<p><b>It is your turn to sign.</b></p>" if v.get("my_turn") else
f"<p>This contract signs in order; it is waiting on {esc(', '.join(v.get('waiting_on') or []))} first.</p>")
hidden = (f"<input type='hidden' name='version' value='{esc(str(v.get('version')))}'>"
f"<input type='hidden' name='sha256' value='{esc(v.get('sha256') or '')}'>")
return public.Page(f"Sign: {v.get('title')}", (
(f"<p style='color:#b00'>{esc(notice)}</p>" if notice else "")
+ f"<p>From {esc(v.get('our_name') or '')}, for {esc(v.get('signer_name') or '')}.</p>"
+ f"<p>Version {esc(str(v.get('version')))} · fingerprint (sha256) <code>{esc(v.get('sha256') or '')}</code></p>"
+ f"<form method='post'><input type='hidden' name='action' value='download'>{hidden}"
"<button>Download the PDF</button></form>"
+ f"<div style='border:1px solid #ccc;padding:1rem;margin:1rem 0'>{text}</div>"
+ turn
+ "<form method='post'><input type='hidden' name='action' value='sign'>" + hidden
+ f"<p><label><input type='checkbox' name='consent' value='yes'> {esc(v.get('consent_text') or '')}</label>"
f"<input type='hidden' name='consent_text_version' value='{esc(v.get('consent_text_version') or '')}'></p>"
+ "<p><label>Type your full name <input name='typed_name' autocomplete='name'></label></p>"
"<p><button>Sign electronically</button></p></form>"
+ f"<p style='font-size:.9rem'>{esc(v.get('retention') or '')} This is a simple electronic signature: a record "
"of who signed, when, from where, and the fingerprint of the document is kept with it.</p>"
+ "<details><summary>Decline, or ask for changes</summary>"
"<form method='post'><input type='hidden' name='action' value='decline'>" + hidden
+ "<p><label>Why are you declining? <textarea name='reason'></textarea></label></p><button>Decline</button></form>"
"<form method='post'><input type='hidden' name='action' value='changes'>" + hidden
+ "<p><label>What should change? <textarea name='note'></textarea></label></p>"
"<button>Ask for changes</button></form></details>"))
@CT.public("SIGN", subject_kind="contract")
def signing_page(link, req):
if link.used_at is not None:
raise Refused(404, "not found")
v = signing.open_for_signing(link.actor, link.subject_id, ip=req.ip, user_agent=req.user_agent)
return _page(v | {"signer_name": link.recipient})
@signing_page.act(single_use=False)
def signing_act(link, req):
f = req.form or {}
action = (f.get("action") or "").strip()
try:
version = int(f.get("version") or 0)
except ValueError:
version = 0
sha = (f.get("sha256") or "").strip()
if action == "download":
content, name = signing.frozen_pdf(link.actor, link.subject_id)
return public.Download(content, "application/pdf", name)
if action == "sign":
signing.consent_and_sign(link.actor, link.subject_id, typed_name=f.get("typed_name") or "",
consented=(f.get("consent") or "").lower() in ("yes", "on", "true", "1"),
version=version, sha256=sha, ip=req.ip, user_agent=req.user_agent, link=link)
return public.Page("Signed", "<p>Thank you — you have signed electronically. Once everyone has signed you will "
"be sent a link to download the signed copy.</p>")
if action == "decline":
signing.decline(link.actor, link.subject_id, reason=f.get("reason") or "", version=version, sha256=sha,
ip=req.ip, user_agent=req.user_agent, link=link)
return public.Page("Declined", "<p>You have declined. The sender has been told.</p>")
if action == "changes":
signing.request_changes(link.actor, link.subject_id, note=f.get("note") or "", version=version, sha256=sha,
ip=req.ip, user_agent=req.user_agent, link=link)
return public.Page("Changes asked for", "<p>Your note has been sent. You will get a new link for the revised "
"version.</p>")
raise Refused(422, "choose sign, decline, ask for changes or download")
@CT.public("READ", subject_kind="contract")
def signed_copy_page(link, req):
content, name = signing.signed_copy(link.actor, link.subject_id)
return public.Download(content, "application/pdf", name)
__all__ = ["signing_page", "signing_act", "signed_copy_page"]