Files
paliad/internal/db/migrations/148_submission_sections.up.sql
mAi e2969fc358
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(submissions): Composer Slice A — base picker + read-only section list (m/paliad#141)
The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.

Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
  (slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
  jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
  users, mutations admin-only (handler-enforced, no RLS write paths).
  Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
  _skeleton.docx. Each section_spec carries the 10-section default
  (letterhead, caption, introduction, requests, facts, legal_argument,
  evidence, exhibits, closing, signature) with bilingual labels +
  bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
  composer_meta jsonb (default '{}'). Purely additive; pre-Composer
  drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
  section_key, order_index, kind ∈ {prose,requests,evidence},
  label_de/en, included, content_md_de/en). RLS mirrors
  submission_drafts (owner-scoped + can_see_project, four policies).

Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
  GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
  multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
  the firm default base and seeds base_id + section rows in one tx.
  Composer wiring is additive — when bases==nil the service stays
  v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
  on both project-scoped and global drafts accept "base_id".

Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
  (hidden until catalog loads); section-list pane above the preview
  (hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
  paintBasePicker rebuilds <option> list on every paint; onBaseChange
  PATCHes base_id and repaints; paintSectionList renders each section
  read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
  Pre-Composer drafts get the picker (catalog still loads) but the
  section pane stays hidden until they pick a base on a new draft.

Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
  (pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
  seeded 10 default sections on both bases; GetDefaultForCode picks
  hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
  base_id + 10 section rows in tx with ascending order_index and
  bilingual labels populated.

NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.

Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).

t-paliad-313
2026-05-26 19:23:40 +02:00

117 lines
5.0 KiB
SQL

-- t-paliad-313 (m/paliad#141): Composer Slice A — per-draft section rows.
--
-- paliad.submission_sections holds one row per (draft, section_key) for
-- Composer-mode drafts. Slice A seeds rows on draft create from the
-- base's section_spec.defaults; the editor renders them read-only. Slice
-- B turns them editable, Slice F adds reorder/hide/add-custom.
--
-- kind values per the design (Q10 ratification — no *_auto kind):
-- 'prose' — free Markdown content (default).
-- 'requests' — Anträge-style content (editor may add auto-numbering
-- later; Slice A treats identical to 'prose').
-- 'evidence' — Beweisangebote (editor may prefix lines with
-- 'Beweis: '; Slice A treats identical to 'prose').
--
-- Visibility flows through draft_id → submission_drafts → can_see_project
-- + owner-scoped. RLS policies mirror the four-policy shape on
-- submission_drafts so seeding from the Go service stays inside the
-- same RLS envelope.
--
-- content_md_de + content_md_en both NOT NULL DEFAULT '' so neither
-- side blocks the bilingual-by-construction render path. Empty content
-- renders as the missing-content marker per the editor's contract.
--
-- Per the brief (head's instruction msg #2392) Slice A does NOT auto-
-- upgrade the 11 pre-Composer drafts — those remain base_id=NULL with
-- no section rows. The v1 fallback render path stays compiled in to
-- keep them working.
CREATE TABLE IF NOT EXISTS paliad.submission_sections (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
draft_id uuid NOT NULL REFERENCES paliad.submission_drafts(id) ON DELETE CASCADE,
section_key text NOT NULL,
order_index int NOT NULL,
kind text NOT NULL,
label_de text NOT NULL,
label_en text NOT NULL,
included bool NOT NULL DEFAULT true,
content_md_de text NOT NULL DEFAULT '',
content_md_en text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT submission_sections_kind_check
CHECK (kind IN ('prose', 'requests', 'evidence')),
CONSTRAINT submission_sections_unique_per_draft
UNIQUE (draft_id, section_key)
);
CREATE INDEX IF NOT EXISTS submission_sections_draft_idx
ON paliad.submission_sections (draft_id, order_index);
ALTER TABLE paliad.submission_sections ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS submission_sections_select ON paliad.submission_sections;
CREATE POLICY submission_sections_select
ON paliad.submission_sections FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM paliad.submission_drafts d
WHERE d.id = paliad.submission_sections.draft_id
AND d.user_id = auth.uid()
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
)
);
DROP POLICY IF EXISTS submission_sections_insert ON paliad.submission_sections;
CREATE POLICY submission_sections_insert
ON paliad.submission_sections FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM paliad.submission_drafts d
WHERE d.id = paliad.submission_sections.draft_id
AND d.user_id = auth.uid()
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
)
);
DROP POLICY IF EXISTS submission_sections_update ON paliad.submission_sections;
CREATE POLICY submission_sections_update
ON paliad.submission_sections FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM paliad.submission_drafts d
WHERE d.id = paliad.submission_sections.draft_id
AND d.user_id = auth.uid()
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM paliad.submission_drafts d
WHERE d.id = paliad.submission_sections.draft_id
AND d.user_id = auth.uid()
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
)
);
DROP POLICY IF EXISTS submission_sections_delete ON paliad.submission_sections;
CREATE POLICY submission_sections_delete
ON paliad.submission_sections FOR DELETE TO authenticated
USING (
EXISTS (
SELECT 1 FROM paliad.submission_drafts d
WHERE d.id = paliad.submission_sections.draft_id
AND d.user_id = auth.uid()
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
)
);
DROP TRIGGER IF EXISTS submission_sections_set_updated_at ON paliad.submission_sections;
CREATE TRIGGER submission_sections_set_updated_at
BEFORE UPDATE ON paliad.submission_sections
FOR EACH ROW EXECUTE FUNCTION paliad.tg_set_updated_at();
COMMENT ON TABLE paliad.submission_sections IS
't-paliad-313: per-draft Composer section rows. Slice A: seeded on draft create from base.section_spec.defaults, rendered read-only. Slice B: editable. RLS mirrors submission_drafts (owner-scoped + can_see_project).';