Files
paliad/pkg/docforge/store.go
mAi b746ec36c7 feat(docforge): slice 7 — generation on uploaded templates (t-paliad-349)
A submission draft can now render from an uploaded docforge template
instead of a legacy Gitea base. DB-VERIFIED against TEST_DATABASE_URL (the
head greenlit option C) before commit — not just compiled.

Schema: migration 159 adds submission_drafts.template_version_id (nullable,
FK template_versions ON DELETE SET NULL) — the snapshot pin (PRD A3). A
later template edit creates a new version; the pinned draft keeps rendering
its version.

Draft service: TemplateVersionID on the model + draftColumns + the JOIN
list + DraftPatch (two-level pointer like base_id) + Update SET. Column-sync
verified live (Create_seeds_section_rows + the new pin test both pass).

Export/preview (handlers): a template-version path checked FIRST — load the
carrier via TemplateStore.GetVersion, render via the existing Export/
RenderPreview (the carrier already carries {{slots}}; no Composer/sections
needed). Falls through to base_id / v1 if the pin is missing. Both preview
sites + the view assembly branch on it.

Store: TemplateMeta.VersionID exposes the current version's row id (slice-4
gap — a consumer needs it to pin); populated in List/Get/GetVersion + the
authoring JSON. New GET /api/templates (authenticated, firm-filtered) is the
picker list any lawyer reads; admin authoring endpoints stay gated.

Frontend: the submission editor's base picker now offers uploaded templates
as a 'tpl:<version_id>' optgroup; selecting one PATCHes template_version_id
(clearing base_id) and vice versa — mutually exclusive render paths.

Live test (submission_draft_template_live_test.go, gated): pin round-trips
Update→Get, the uploaded carrier renders ({{firm.name}}→HLC via Export), and
clearing nulls it — all PASS against real Postgres.

Verification: go build/vet/gofmt clean; bun build + bun test 274/274; slice-7
+ slice-4 store + draft/composer live tests PASS against TEST_DATABASE_URL.
Pre-existing env failures (approval/projection seed $1-type quirk,
migration136 stale deadline_rules table) are unrelated — confirmed my branch
touches none of that code.

m/paliad#157
2026-05-29 17:55:31 +02:00

112 lines
4.5 KiB
Go

package docforge
import "context"
// TemplateMeta is the listable metadata for a stored template — cheap to
// list because it carries no carrier bytes.
type TemplateMeta struct {
ID string
Slug string // optional human handle; may be empty
NameDE string
NameEN string
Kind string // consumer-domain tag, e.g. "submission"
SourceFormat string // "docx"
Firm string // may be empty
IsActive bool
Version int // current version number; 0 when no version exists yet
VersionID string // current version row id; "" when no version exists yet.
// A draft pins VersionID to snapshot this exact version (PRD §4 A3):
// a later template edit creates a new version and re-points current,
// but the pinned draft keeps rendering VersionID.
}
// TemplateSlot is one variable slot placed in a template version's carrier.
type TemplateSlot struct {
// Key is the variable bound here, in the placeholder grammar
// (e.g. "project.case_number").
Key string
// Anchor locates the slot within the carrier. With the sentinel
// strategy this is the token the authoring surface injected into the
// carrier OOXML at the slot position.
Anchor string
// Label is an optional human label for the authoring palette.
Label string
// OrderIndex orders slots for display.
OrderIndex int
}
// Template is a stored template resolved to its current version: metadata
// plus everything needed to author or generate — the carrier bytes, the
// stylemap, and the placed slots. CarrierBytes is format-opaque; the .docx
// adapter wraps (CarrierBytes, Stylemap) into a docx.Carrier at compose
// time, so this root type never imports the adapter.
type Template struct {
TemplateMeta
CarrierBytes []byte
Stylemap map[string]string
Slots []TemplateSlot
}
// TemplateMetaInput is the payload for creating a new template (the
// catalog row). ID and Version are assigned by the store.
type TemplateMetaInput struct {
Slug string // optional
NameDE string
NameEN string
Kind string // defaults to "submission" when empty
SourceFormat string // defaults to "docx" when empty
Firm string // optional
CreatedBy string // auth user id (uuid) for the audit column
}
// TemplateVersionInput is the payload for creating a template version: the
// carrier .docx, its stylemap, and the slots placed in it.
type TemplateVersionInput struct {
CarrierBytes []byte
Stylemap map[string]string
Slots []TemplateSlot
CreatedBy string // auth user id (uuid)
}
// TemplateFilter narrows a List. Zero-value fields mean "any".
type TemplateFilter struct {
Firm string // "" = any firm
Kind string // "" = any kind
ActiveOnly bool // true = is_active templates only
}
// TemplateStore persists and loads document templates. docforge defines
// the contract; the consuming application implements it (paliad against
// Postgres, with the carrier bytes in a bytea column). It is the seam the
// authoring surface writes to and the generator reads from — a second
// docforge consumer implements the same interface against its own storage.
//
// Versioning is snapshot-at-create (PRD §4 A3): Create makes version 1 and
// pins it as current; AddVersion inserts the next version and re-points
// current. Drafts pin a specific version so a later edit never shifts an
// in-flight draft.
type TemplateStore interface {
// List returns catalog metadata for templates matching the filter,
// without carrier bytes.
List(ctx context.Context, f TemplateFilter) ([]TemplateMeta, error)
// Get returns a template resolved to its current version (carrier +
// stylemap + slots). Returns ErrTemplateNotFound when id is unknown.
Get(ctx context.Context, id string) (*Template, error)
// GetVersion returns a template resolved to a specific version id —
// the path a draft uses to render its pinned snapshot. Returns
// ErrTemplateNotFound when the version is unknown.
GetVersion(ctx context.Context, versionID string) (*Template, error)
// Create inserts a new template plus its first version (version 1) and
// pins that version as current. Returns the resolved Template.
Create(ctx context.Context, meta TemplateMetaInput, first TemplateVersionInput) (*Template, error)
// AddVersion inserts the next version for an existing template and
// re-points current_version to it. Returns the resolved Template at
// the new version. Returns ErrTemplateNotFound when templateID is
// unknown.
AddVersion(ctx context.Context, templateID string, v TemplateVersionInput) (*Template, error)
}