-- t-paliad-265 / m/paliad#96 — per-event-card optional choices on the -- Verfahrensablauf timeline. -- -- Design: docs/design-event-card-choices-2026-05-25.md -- Decisions: see §11 of the design doc. -- -- Two schema changes: -- -- 1. paliad.project_event_choices — new persistence table holding the -- user's per-card picks scoped to a project. One row per -- (project, submission_code, choice_kind). Re-picking is an UPDATE -- (UNIQUE constraint enforces idempotence). -- -- 2. paliad.deadline_rules.choices_offered jsonb — opt-in declaration -- of which choice-kinds each rule offers. The projection engine -- reads this to decide whether to render the caret affordance on -- a card. Seeded for every event_type='decision' rule (appellant), -- every priority='optional' rule (skip), and the two Klageerwiderung -- rows (include_ccr). -- -- NOTE on join key: the design doc named the join column "rule_code". -- Live verification (2026-05-25 SELECT against paliad.deadline_rules) -- showed `rule_code` is NULL on every decision row — it's the legal- -- source citation column, not a stable identifier. The -- AnchorOverrides plumbing in internal/services/fristenrechner.go -- already keys on `submission_code` (UIDeadline.Code populates from -- submission_code, lines 351-352), so we mirror that decision here: -- the join column is `submission_code`. Same intent, correct field. -- -- Idempotent: CREATE TABLE IF NOT EXISTS + ADD COLUMN IF NOT EXISTS + -- UPDATEs guarded by WHERE choices_offered IS NULL so re-applying -- against an already-seeded DB no-ops. SELECT set_config( 'paliad.audit_reason', 'mig 129: add paliad.project_event_choices + deadline_rules.choices_offered for per-event-card optional choices (t-paliad-265 / m/paliad#96)', true); -- 1. The choice-storage table ---------------------------------------------- CREATE TABLE IF NOT EXISTS paliad.project_event_choices ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), project_id uuid NOT NULL REFERENCES paliad.projects(id) ON DELETE CASCADE, submission_code text NOT NULL, choice_kind text NOT NULL CHECK (choice_kind IN ('appellant', 'include_ccr', 'skip')), choice_value text NOT NULL, created_by uuid REFERENCES paliad.users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_by uuid REFERENCES paliad.users(id) ON DELETE SET NULL, updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (project_id, submission_code, choice_kind) ); CREATE INDEX IF NOT EXISTS project_event_choices_project_idx ON paliad.project_event_choices (project_id); ALTER TABLE paliad.project_event_choices ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS project_event_choices_select ON paliad.project_event_choices; CREATE POLICY project_event_choices_select ON paliad.project_event_choices FOR SELECT USING (paliad.can_see_project(project_id)); DROP POLICY IF EXISTS project_event_choices_mutate ON paliad.project_event_choices; CREATE POLICY project_event_choices_mutate ON paliad.project_event_choices FOR ALL USING (paliad.can_see_project(project_id)) WITH CHECK (paliad.can_see_project(project_id)); COMMENT ON TABLE paliad.project_event_choices IS 'Per-event-card user picks scoped to a project. choice_kind ∈ {appellant, include_ccr, skip}. ' 'choice_value namespace per kind: appellant=claimant|defendant|both|none; include_ccr=true|false; ' 'skip=true|false. Join key submission_code matches paliad.deadline_rules.submission_code (the same key ' 'AnchorOverrides uses). UNIQUE(project,submission_code,kind) keeps re-picks idempotent. ' 'Audit-logged via paliad.system_audit_log (event_type=project_event_choice.set).'; -- 2. The choices_offered opt-in column ------------------------------------ ALTER TABLE paliad.deadline_rules ADD COLUMN IF NOT EXISTS choices_offered jsonb; COMMENT ON COLUMN paliad.deadline_rules.choices_offered IS 'Declares which per-card choice-kinds this rule offers on the Verfahrensablauf timeline. ' 'NULL = no caret affordance (default). Example shapes: ' '{"appellant": ["claimant","defendant","both","none"]} on decision rules, ' '{"skip": [true, false]} on optional rules, ' '{"include_ccr": [true, false]} on Klageerwiderung rules. ' 'Engine and frontend read it; storing per-kind value lists keeps the contract self-describing.'; -- 3. Seed ----------------------------------------------------------------- -- 3a. Every published decision rule offers the appellant choice. UPDATE paliad.deadline_rules SET choices_offered = '{"appellant": ["claimant", "defendant", "both", "none"]}'::jsonb WHERE event_type = 'decision' AND lifecycle_state = 'published' AND choices_offered IS NULL; -- 3b. Every published optional rule offers the skip choice. UPDATE paliad.deadline_rules SET choices_offered = '{"skip": [true, false]}'::jsonb WHERE priority = 'optional' AND lifecycle_state = 'published' AND choices_offered IS NULL; -- 3c. Klageerwiderung rules offer the include_ccr choice. Two rows -- today (upc.inf.cfi.sod + de.inf.lg.erwidg) — verified live -- (2026-05-25 SELECT FROM paliad.deadline_rules WHERE name ILIKE -- 'Klageerwiderung'); the UPC INF Klageerwiderung is `sod` (Statement -- of Defence, R.24 RoP), not `def`. Slice B (Q4 bundle) is the -- user-visible feature. UPDATE paliad.deadline_rules SET choices_offered = '{"include_ccr": [true, false]}'::jsonb WHERE submission_code IN ('upc.inf.cfi.sod', 'de.inf.lg.erwidg') AND lifecycle_state = 'published' AND choices_offered IS NULL;