m/paliad#96 — per-event-card optional choices on the Verfahrensablauf timeline. This commit lands the schema + service layer. Migration 129: - paliad.project_event_choices table (project_id, submission_code, choice_kind ∈ {appellant, include_ccr, skip}, choice_value) with UNIQUE(project_id, submission_code, choice_kind) for idempotent re-pick, RLS via paliad.can_see_project. - paliad.deadline_rules.choices_offered jsonb — opt-in declaration of which choice-kinds each rule offers. Seeded for every decision rule (appellant), every priority='optional' rule (skip), and the two Klageerwiderung rules (upc.inf.cfi.sod + de.inf.lg.erwidg) with include_ccr. Live verification before authoring: - rule_code is NULL on every decision row → submission_code is the join key (matches AnchorOverrides plumbing in fristenrechner.go). - upc.inf.cfi.sod is the UPC Klageerwiderung, not upc.inf.cfi.def (rejected the design doc's first guess; SELECT name ILIKE 'Klageerwiderung' confirmed). Go service: - models.ProjectEventChoice + DeadlineRule.ChoicesOffered. - EventChoiceService: ListForProject / Upsert (with audit-log row to paliad.system_audit_log) / Delete. Pure-helper ToCalcOptionsAddendum + per-kind value validation + unit tests. Design: docs/design-event-card-choices-2026-05-25.md §3 + §6.
117 lines
5.5 KiB
SQL
117 lines
5.5 KiB
SQL
-- 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;
|