Files
paliad/internal/db/migrations/134_berufung_unification.up.sql
mAi e2d75c391d
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
fix(litigationplanner): rename upc.apl → upc.apl.unified (HOTFIX, t-paliad-299, m/paliad#130)
mig 134 was inserting code='upc.apl' (2 segments) into paliad.proceeding_types,
which carries paliad_proceeding_code_shape CHECK requiring 3 dot-segments OR
'^_archived_'. Every container restart hit the constraint, rolled the migration
TXN back, and crash-looped paliad.de.

Rename the unified Berufung code to 'upc.apl.unified' (3 segments, satisfies the
constraint, preserves design intent). The pre-existing constraint is a useful
jurisdiction.category.specific invariant — keep it, fix the new row.

Touched only string literals:
- mig 134 up.sql + down.sql (insert, lookups, post-checks)
- frontend/src/verfahrensablauf.tsx (UPC_TYPES code + i18nKey)
- frontend/src/client/verfahrensablauf.ts (APPELLANT_AXIS + APPEAL_TARGET sets)
- frontend/src/client/i18n.ts (DE + EN translation rows)
- frontend/src/i18n-keys.ts (auto-regen via bun build)
- internal/services/lookup_events_test.go (anchor-row assertion)

Verified: `grep -rn "'upc\.apl'\|\"upc\.apl\""` returns zero hits.
go build, bun run build, go test ./... all green.
2026-05-26 15:09:12 +02:00

264 lines
10 KiB
SQL

-- 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.unified',
'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.unified';
RAISE NOTICE '[mig 134] new upc.apl.unified proceeding_type_id = %', upc_apl_id;
RAISE NOTICE '[mig 134] Rules to reassign to upc.apl.unified 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.unified proceeding_type row.
UPDATE paliad.deadline_rules dr
SET proceeding_type_id = (
SELECT id FROM paliad.proceeding_types WHERE code = 'upc.apl.unified'
)
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.unified' AND dr.is_active = true;
RAISE NOTICE '[mig 134] post: rules on unified upc.apl.unified = % (expected 16)', unified_count;
IF unified_count <> 16 THEN
RAISE EXCEPTION '[mig 134] FAILED — expected 16 rules on upc.apl.unified, 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.unified' 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.
-- ---------------------------------------------------------------