Letters through the full workflow chain (approval + sign + e-Meterai + e-Stamp) — design spec (DRAFT)
Status: DRAFT for review. Requested 2026-07-24 ("Re-use workflow engine for 'Submit for Approval', so that there's a chain for approval, signature, e-meterai, e-stamp"). Not yet approved — open questions at the end.
Goal
Give a correspondence letter the same definition-driven workflow chain a document already gets — an ordered run of approve → sign → e-Meterai → e-Stamp steps — instead of the current single-round approval-only path. Reuse the CORE workflow engine and the esign crypto/ledger unchanged; the work is a letter-subject content adapter for the seal I/O.
Current state (verified against code)
SubmitLetter(handlers_correspondence.go) opens the built-inworkflow.Start— a single implicit approval round oversubject_type='letter', approvers chosen ad-hoc. The terminal Approved/Rejected is mirrored back to the letter status byActWorkflow→ReflectApprovalState. The built-inActknows only approve/reject/return/cancel — no step/ceremony machinery.- The definition-driven path (
StartFromDefinition/StartCustomWorkflow/ActDefinition) DOES carry ceremony steps:StepKindApprove/Sign/Meterai/SignExternal/Stamp(workflow/domain/definition.go), validated ordering (meterai follows a sign; stamp is last). The workflow engine core (Instance, tasks, inbox, history, definitions, state machine) is subject-agnostic —SubjectType/SubjectIDare free strings and tests already run definitions over"letter". - The esign crypto + evidence ledger (
signatures,meterai_records,stamp_records, envelopes, pending-seals) is already keyed on(subject_type, subject_id)— it would accept"letter"unchanged. TheSign/AffixMeterai/AffixStampapp methods are pure bytes-in → sealed-bytes-out.
The one blocker: seal/sign PDF I/O is document-version-coupled
The sign/meterai/stamp ceremony reads "the current version PDF" and writes "a new signed version" exclusively through DMS document_versions:
- Ports VersionReader.CurrentVersionPDF(docID) / VersionWriter.AddSignedVersion(docID, pdf, kind) (esign/app/service.go), wired to s.dms.* in resolvers.go.
- The synchronous ceremony handlers (handlers_esign.go) do their own s.dms.OpenVersionContent(docID, version) → seal → s.dms.AddVersion(docID, ...), mounted on /documents/{docID}/... routes.
- Every Complete{Signature,Meterai,Stamp}ForSubject hook and every envelope/pending-seal creation is called with a hardcoded "document".
A letter has no document_versions row — its rendered PDF is a single content-addressed blob at letters.content_hash. So none of the seal I/O can serve a letter as-is.
Proposed design
1. A subject-generic content port (the load-bearing change)
Introduce a SealSubject port keyed on (subjectType, subjectID):
CurrentPDF(ctx, subjectType, subjectID) (pdf []byte, rev int, err error)
WriteSealedPDF(ctx, signer, subjectType, subjectID, pdf []byte, kind string) (rev int, err error)
Two implementations, dispatched by subjectType:
- document → today's s.dms.* (unchanged behavior).
- letter → reads letters.content_hash; writes the sealed PDF back as the new letters.content_hash plus a new letter_seals history row (see §3) so the letter keeps an audit trail of its sealed revisions (letters have no version table; this is the minimal equivalent).
Replace the VersionReader/VersionWriter calls in esign + the sync handlers' direct dms.OpenVersionContent/dms.AddVersion with this port, and pass the real (subjectType, subjectID) into every Complete*ForSubject hook (retire the "document" literals).
2. Drive letters through the definition path
SubmitLetter gains a mode: instead of the built-in Start, call StartCustomWorkflow/StartFromDefinition with a letter-appropriate definition (a chosen saved workflow, or a custom builder run — the same UI documents use). The letter's status mirrors the run's terminal state via the existing ReflectApprovalState (extended: a fully-seal-completed run → numbered? or a new sealed status — open question). Numbering (AssignNumber) still produces the base PDF the chain then signs/affixes.
3. Letter seal history — migration
letter_seals(id, letter_id, rev int, kind text, content_hash text, created_by, created_at) — one row per sealed revision (the letter equivalent of document_versions for the seal trail). letters.content_hash always points at the newest sealed PDF; letter_seals is the history + the source for verification. Voiding (rejected/cancelled runs) marks the seal rows + the esign ledger void, mirroring the document void path (workflowSealVoider — generalize its subjectType=="document" guard).
4. Ordering & numbering interaction
Decide where numbering sits relative to the chain: (a) number first (stamp the official number into the PDF), then sign/affix the numbered PDF — recommended, since the signature should cover the numbered document; or (b) sign the draft then number. The merge-field numbering (docx letters) already produces the final PDF; the chain seals that.
What does NOT change
Workflow engine core, esign crypto, the signatures/meterai_records/stamp_records ledger, the OTP/KEYLA ceremonies, the inbox/task/history UI. Documents are untouched (their SealSubject impl is today's code behind the new interface).
Effort estimate
Medium-large. The content port + retiring the "document" literals + the letter_seals migration + wiring SubmitLetter to the definition path + generalizing the void guard + placement-gating decision (letters: skip page-placement UI, or reuse it?). The esign/ledger/engine need no change. Realistically a multi-task plan (~6–8 tasks) with an adversarial review on the seal-I/O generalization (it touches the document seal path).
Open questions (resolve before the plan)
- Placement: documents let a signer place the signature on a page. Do letters need page-placement, or a fixed footer block? (Simpler: fixed/last-page for letters v1.)
- Numbering vs. chain order (§4) — recommend number-then-seal.
- Terminal status: does a fully-sealed letter become
numbered, or a newsealed/dispatchedstatus? - Definition source: a fixed "letter approval+sign" preset, or the full custom-workflow builder targeting letters? (Recommend: reuse the builder + saved workflows, subject=letter.)
- External signers (
sign_external) for letters (counter-party signs)? In scope or defer?
Relationship to the other draft
Pairs with 2026-07-24-correspondence-document-view-design.md — that spec gives letters the Signatures/Workflow tabs (the UI surface for this chain). Build order: this backend chain first (or together), then surface it in the document-style letter view.