think
16px
820px

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-in workflow.Start — a single implicit approval round over subject_type='letter', approvers chosen ad-hoc. The terminal Approved/Rejected is mirrored back to the letter status by ActWorkflowReflectApprovalState. The built-in Act knows 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-agnosticSubjectType/SubjectID are 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. The Sign/AffixMeterai/AffixStamp app 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)

  1. 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.)
  2. Numbering vs. chain order (§4) — recommend number-then-seal.
  3. Terminal status: does a fully-sealed letter become numbered, or a new sealed/dispatched status?
  4. Definition source: a fixed "letter approval+sign" preset, or the full custom-workflow builder targeting letters? (Recommend: reuse the builder + saved workflows, subject=letter.)
  5. 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.