-- 134_berufung_unification — Slice B1, m/paliad#124, t-paliad-298+ -- -- Collapses the 3 active UPC appeal proceeding_types (upc.apl.merits, -- upc.apl.cost, upc.apl.order — 16 rules across 3 codes) into ONE -- unified upc.apl proceeding type + an `appeal_target` discriminator on -- both proceeding_types (top-level marker) and deadline_rules -- (per-row applies-to set, text[] for multi-target rules). -- -- ADDITIVE ONLY. The migration: -- 1. Adds the two columns + check constraints. -- 2. Inserts the new upc.apl proceeding type. -- 3. Audit-first: NOTICES every row about to be touched. -- 4. Reassigns rule rows from the 3 old types to upc.apl, stamping -- applies_to_target by source proceeding code. -- 5. Archives (is_active=false) the 3 old proceeding_types — NEVER -- deletes them, so any historical project_event_choices / FK -- references stay intact. -- -- Schadensbemessung + Bucheinsicht get NO rule rows in this migration -- (m's 2026-05-26 decision: distinct rule sets, not shared with -- merits). Their appeal_target enum values are defined and addressable -- by CalcOptions.AppealTarget; the engine returns an empty timeline -- until rules are seeded in a follow-up slice (likely via -- /admin/rules, pairing with t-paliad-193 orphan-concept-seed). -- -- See docs/design-litigation-planner-2026-05-26.md §18.1. -- --------------------------------------------------------------- -- 1. Schema additions -- --------------------------------------------------------------- ALTER TABLE paliad.proceeding_types ADD COLUMN appeal_target text NULL; ALTER TABLE paliad.proceeding_types ADD CONSTRAINT proceeding_types_appeal_target_chk CHECK (appeal_target IS NULL OR appeal_target IN ( 'endentscheidung', 'kostenentscheidung', 'anordnung', 'schadensbemessung', 'bucheinsicht' )); COMMENT ON COLUMN paliad.proceeding_types.appeal_target IS 'Top-level appeal-target marker. NULL on non-appeal proceedings. ' 'Reserved for future variants — today only the unified upc.apl row ' 'has this NULL (the actual per-rule target set lives on ' 'paliad.deadline_rules.applies_to_target).'; ALTER TABLE paliad.deadline_rules ADD COLUMN applies_to_target text[] NULL; ALTER TABLE paliad.deadline_rules ADD CONSTRAINT deadline_rules_applies_to_target_chk CHECK ( applies_to_target IS NULL OR applies_to_target <@ ARRAY[ 'endentscheidung', 'kostenentscheidung', 'anordnung', 'schadensbemessung', 'bucheinsicht' ]::text[] ); COMMENT ON COLUMN paliad.deadline_rules.applies_to_target IS 'Set of appeal_target slugs this rule applies to. NULL on rules ' 'that don''t belong to an appeal proceeding. The engine filters ' 'by CalcOptions.AppealTarget — rules whose applies_to_target ' 'contains the requested slug are emitted; others are suppressed.'; -- --------------------------------------------------------------- -- 2. Insert the unified upc.apl row. -- -- Inherits default_color from the merits row (the most-used appeal -- track today). sort_order follows the cluster of UPC proceedings; -- placed just before upc.apl.merits's old slot so the chip-grouped -- picker UI lands Berufung in a sensible position. Tweakable later -- without a migration. -- --------------------------------------------------------------- INSERT INTO paliad.proceeding_types ( code, name, name_en, description, jurisdiction, category, default_color, sort_order, is_active, display_order, appeal_target ) SELECT 'upc.apl', 'Berufungsverfahren', 'Appeal', 'Vereinheitlichtes Berufungsverfahren — wählen Sie anschließend, ' 'worauf die Berufung sich richtet (Endentscheidung, ' 'Kostenentscheidung, Anordnung, Schadensbemessung, Bucheinsicht).', 'UPC', 'fristenrechner', default_color, sort_order, true, display_order, NULL FROM paliad.proceeding_types WHERE code = 'upc.apl.merits'; -- --------------------------------------------------------------- -- 3. Audit-first RAISE NOTICE pass. -- -- Lists every rule row that will be reassigned + every proceeding_type -- row that will be archived. The migration runs to completion either -- way; the operator reads the notices to confirm scope before the -- next migration in the chain. -- --------------------------------------------------------------- DO $$ DECLARE rec record; upc_apl_id int; rules_touched int := 0; procs_archived int := 0; BEGIN SELECT id INTO upc_apl_id FROM paliad.proceeding_types WHERE code = 'upc.apl'; RAISE NOTICE '[mig 134] new upc.apl proceeding_type_id = %', upc_apl_id; RAISE NOTICE '[mig 134] Rules to reassign to upc.apl with applies_to_target:'; FOR rec IN SELECT dr.id AS rule_id, pt.code AS old_proceeding, dr.submission_code, dr.name FROM paliad.deadline_rules dr JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id WHERE pt.code IN ('upc.apl.merits', 'upc.apl.cost', 'upc.apl.order') AND dr.is_active = true ORDER BY pt.code, dr.sequence_order LOOP RAISE NOTICE '[mig 134] % % % (%)', rec.old_proceeding, rec.submission_code, rec.name, rec.rule_id; rules_touched := rules_touched + 1; END LOOP; RAISE NOTICE '[mig 134] Total rules to reassign: %', rules_touched; RAISE NOTICE '[mig 134] Proceeding_types to archive (is_active=false):'; FOR rec IN SELECT id, code, name FROM paliad.proceeding_types WHERE code IN ('upc.apl.merits', 'upc.apl.cost', 'upc.apl.order') ORDER BY sort_order LOOP RAISE NOTICE '[mig 134] % % (id=%)', rec.code, rec.name, rec.id; procs_archived := procs_archived + 1; END LOOP; RAISE NOTICE '[mig 134] Total proceeding_types to archive: %', procs_archived; END $$; -- --------------------------------------------------------------- -- 4. Reassign rule rows. -- -- Stamp applies_to_target by source proceeding code, then point all -- 16 rules at the new upc.apl row. -- --------------------------------------------------------------- -- 4a. upc.apl.merits → applies_to_target = {endentscheidung} UPDATE paliad.deadline_rules dr SET applies_to_target = ARRAY['endentscheidung']::text[] FROM paliad.proceeding_types pt WHERE pt.id = dr.proceeding_type_id AND pt.code = 'upc.apl.merits' AND dr.is_active = true; -- 4b. upc.apl.cost → applies_to_target = {kostenentscheidung} UPDATE paliad.deadline_rules dr SET applies_to_target = ARRAY['kostenentscheidung']::text[] FROM paliad.proceeding_types pt WHERE pt.id = dr.proceeding_type_id AND pt.code = 'upc.apl.cost' AND dr.is_active = true; -- 4c. upc.apl.order → applies_to_target = {anordnung} UPDATE paliad.deadline_rules dr SET applies_to_target = ARRAY['anordnung']::text[] FROM paliad.proceeding_types pt WHERE pt.id = dr.proceeding_type_id AND pt.code = 'upc.apl.order' AND dr.is_active = true; -- 4d. Reassign all 16 rules to the new upc.apl proceeding_type row. UPDATE paliad.deadline_rules dr SET proceeding_type_id = ( SELECT id FROM paliad.proceeding_types WHERE code = 'upc.apl' ) FROM paliad.proceeding_types pt WHERE pt.id = dr.proceeding_type_id AND pt.code IN ('upc.apl.merits', 'upc.apl.cost', 'upc.apl.order'); -- --------------------------------------------------------------- -- 5. Archive the 3 old proceeding_types. -- -- NEVER DELETE — historical project_event_choices and project FKs -- (paliad.projects.proceeding_type_id) may still reference these IDs. -- The is_active=false flag stops them appearing in the picker but -- preserves FK integrity for historical reads. -- --------------------------------------------------------------- UPDATE paliad.proceeding_types SET is_active = false, updated_at = now() WHERE code IN ('upc.apl.merits', 'upc.apl.cost', 'upc.apl.order'); -- --------------------------------------------------------------- -- 6. Post-migration sanity check. -- --------------------------------------------------------------- DO $$ DECLARE unified_count int; archived_count int; target_distribution record; BEGIN SELECT COUNT(*) INTO unified_count FROM paliad.deadline_rules dr JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id WHERE pt.code = 'upc.apl' AND dr.is_active = true; RAISE NOTICE '[mig 134] post: rules on unified upc.apl = % (expected 16)', unified_count; IF unified_count <> 16 THEN RAISE EXCEPTION '[mig 134] FAILED — expected 16 rules on upc.apl, got %', unified_count; END IF; SELECT COUNT(*) INTO archived_count FROM paliad.proceeding_types WHERE code IN ('upc.apl.merits', 'upc.apl.cost', 'upc.apl.order') AND is_active = false; RAISE NOTICE '[mig 134] post: archived old appeal proceeding_types = % (expected 3)', archived_count; IF archived_count <> 3 THEN RAISE EXCEPTION '[mig 134] FAILED — expected 3 archived types, got %', archived_count; END IF; FOR target_distribution IN SELECT unnest(applies_to_target) AS target, COUNT(*) AS n FROM paliad.deadline_rules dr JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id WHERE pt.code = 'upc.apl' AND dr.is_active = true GROUP BY unnest(applies_to_target) ORDER BY 1 LOOP RAISE NOTICE '[mig 134] post: applies_to_target=% count=%', target_distribution.target, target_distribution.n; END LOOP; END $$; -- --------------------------------------------------------------- -- TODO (follow-up slice, not in 134): -- -- Seed rules for Schadensbemessung-as-appeal + Bucheinsicht-as-appeal. -- m's 2026-05-26 decision: distinct rule sets, NOT shared with merits. -- - Schadensbemessung: anchor on R.118.4 decision; conjecture 2/4-month -- merits-style track but distinct legal basis. -- - Bucheinsicht: anchor on R.142 (Lay-open-books decision); conjecture -- 15-day track per R.220.2 + R.224.2.b. -- Can pair with t-paliad-193 orphan-concept-seed if m wants a combined -- editorial pass via /admin/rules. -- ---------------------------------------------------------------