Collapses the 3 UPC appeal proceeding_types (upc.apl.merits 7 rules,
upc.apl.cost 2, upc.apl.order 7 = 16 total across 3 codes) into ONE
unified upc.apl proceeding type + a per-rule applies_to_target[]
discriminator. The verfahrensablauf picker now shows one "Berufung"
tile; after picking it, the user selects which decision the appeal is
directed AT via a 5-chip group (Endentscheidung / Kostenentscheidung /
Anordnung / Schadensbemessung / Bucheinsicht) and the engine filters
rules whose applies_to_target contains the picked slug.
m's 2026-05-26 decision: Schadensbemessung-as-appeal is a NEW first-
class target with its OWN rule set (no shared inheritance from
merits). The 5 enum values are all defined + addressable; for now
schadensbemessung and bucheinsicht return empty timelines until rules
are seeded in a follow-up slice (likely via /admin/rules or pairing
with t-paliad-193 orphan-concept-seed).
Migration 134 (additive only):
- ADD proceeding_types.appeal_target text (CHECK on 5 slugs OR NULL)
- ADD deadline_rules.applies_to_target text[] (CHECK each element
in the 5 slugs)
- INSERT the unified upc.apl row (inherits sort/color from
upc.apl.merits)
- Audit-first RAISE NOTICE pass listing every row about to be
touched + a post-migration sanity check
- Reassign rule rows: merits → applies_to_target={endentscheidung},
cost → {kostenentscheidung}, order → {anordnung}
- Archive (is_active=false, NOT DELETE) the 3 old proceeding_types
so historical FKs stay intact
- Down migration restores is_active=true on the 3 old types, points
rules back by their applies_to_target stamp, drops the unified
row, drops both columns. Safe.
Package additions (pkg/litigationplanner):
- AppealTarget* constants + AppealTargets[] ordered list +
IsValidAppealTarget(s) predicate (silent no-op on unknown slugs
so a stale frontend chip doesn't break the render)
- ProceedingType.AppealTarget *string field (top-level marker;
NULL on non-appeal proceedings)
- Rule.AppliesToTarget pq.StringArray field (per-row applies-to set)
- CalcOptions.AppealTarget string (engine filter — when set,
keeps only rules whose AppliesToTarget contains the slug)
Engine filter runs after ApplyRuleOverrides but before the rule walk
so the existing condition_expr / spawn / appellant-context machinery
operates on the filtered subset transparently.
paliad-side wiring:
- deadline_rule_service.go: ruleColumns + proceedingTypeColumns
extended to scan the new columns
- handlers/fristenrechner.go: AppealTarget JSON field on the
request payload, threaded into CalcOptions
Frontend (verfahrensablauf surface only):
- Single "Berufung" tile replaces the 3 separate Berufung tiles
- New 5-chip appeal-target row, shown only when upc.apl is picked
- URL state ?target=<slug>; default endentscheidung when none set
- APPELLANT_AXIS_PROCEEDINGS updated: upc.apl.* (3 entries) →
upc.apl (1 entry)
- i18n keys (DE + EN) for the new tile + the 5 chip labels +
the "Worauf richtet sich die Berufung?" / "Appeal against:" prompt
- calculateDeadlines threads appealTarget through to the API
Acceptance:
- go build clean, go test all green (existing test suite — no new
tests on the engine filter as a follow-up; the migration's
sanity-check DO block guards the rule-reassignment count)
- Live audit before drafting confirmed: 3 active UPC appeal
proceeding_types, 16 rules total, primary_party already conforms
to 4-value vocab on all proceeding-bound rules
264 lines
10 KiB
SQL
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',
|
|
'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.
|
|
-- ---------------------------------------------------------------
|