feat(litigationplanner): Berufung unification — one upc.apl + 5 appeal_target chips (Slice B1, m/paliad#124 §18.1)
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
This commit is contained in:
@@ -1245,16 +1245,26 @@ Frontend logic:
|
|||||||
5. Existing project rows that referenced the old `upc.apl.merits` / `upc.apl.cost` / `upc.apl.order` codes still load (the FK integrity is preserved via the archived old rows).
|
5. Existing project rows that referenced the old `upc.apl.merits` / `upc.apl.cost` / `upc.apl.order` codes still load (the FK integrity is preserved via the archived old rows).
|
||||||
6. The `paliad.proceeding_type_history` follow-up (not in scope here) can later migrate those project FKs to the new `upc.apl` + `appeal_target` field — that's a follow-up.
|
6. The `paliad.proceeding_type_history` follow-up (not in scope here) can later migrate those project FKs to the new `upc.apl` + `appeal_target` field — that's a follow-up.
|
||||||
|
|
||||||
#### Open question for m (escalate via `mai instruct head`)
|
#### m's answer on Q18.1.1 (2026-05-26 13:40)
|
||||||
|
|
||||||
**Q18.1.1 — Is Schadensbemessung-as-appeal a duplicate of Endentscheidung, or a distinct set?**
|
> Schadensbemessung-as-appeal is a NEW appeal target. Model it as a first-class entry in the appeal_target enum with its own rule set (no shared inheritance from upc.apl.merits). The rules don't exist yet in the catalog; for now the appeal_target value is defined and `CalcOptions.AppealTarget="schadensbemessung"` returns an empty sequence until rules are seeded.
|
||||||
|
|
||||||
Three interpretations:
|
→ **Option B wins (m overrode inventor's R=A).** Migration 134 still ships the schema + the 5 enum values + the chip group + the engine filter. Existing rules:
|
||||||
- A. **Shared rule set**: appeal of a Schadensbemessung uses the SAME rules as appeal of an Endentscheidung (both run the 2/4-month merits track). `applies_to_target=['endentscheidung','schadensbemessung']` on each rule. (R) — simplest, matches what the live `upc.apl.merits` corpus does today (no explicit target distinction).
|
- 7 `upc.apl.merits` rules → `applies_to_target=['endentscheidung']` (Endentscheidung only — NOT also Schadensbemessung).
|
||||||
- B. **Distinct rule set**: Schadensbemessung-appeal has its own anchor + sequence (different trigger event, different timing). Would need 7 new rule rows specifically for `applies_to_target=['schadensbemessung']`. No live evidence for this today.
|
- 2 `upc.apl.cost` rules → `applies_to_target=['kostenentscheidung']`.
|
||||||
- C. **Defer**: ship Berufung unification with only 3 targets (endentscheidung / kostenentscheidung / anordnung) for v1; add Schadensbemessung + Bucheinsicht as a follow-up.
|
- 7 `upc.apl.order` rules → `applies_to_target=['anordnung']`.
|
||||||
|
- **Schadensbemessung + Bucheinsicht get NO rules in this migration.** `applies_to_target='schadensbemessung'` and `'bucheinsicht'` are valid enum values but no rule row carries them yet.
|
||||||
|
|
||||||
Recommendation: **A** — share rules, multi-valued `applies_to_target` array. Frontend renders all 5 chips from day 1; the merits 7 rules show under endentscheidung + schadensbemessung; the order 7 rules show under anordnung + bucheinsicht (the 15-day track DOES apply to Bucheinsicht under R.142+R.220.2). No new rule rows needed.
|
**Frontend behaviour with empty rule sets:**
|
||||||
|
- All 5 target chips still render (the picker promises the user a complete vocabulary).
|
||||||
|
- Picking Schadensbemessung or Bucheinsicht returns an empty timeline with a banner: "Frist-Sequenz für diesen Berufungstyp ist noch nicht hinterlegt — bitte über /admin/rules einpflegen oder Migrations-Follow-up abwarten."
|
||||||
|
- Picking the 3 populated targets renders normally.
|
||||||
|
|
||||||
|
**Rule-seeding follow-up (TODO, separate slice):**
|
||||||
|
- Schadensbemessung-appeal rules: anchor on R.118.4 (Folgeentscheidung Schadensbemessung) decision; conjecture 2/4-month track but distinct legal basis.
|
||||||
|
- Bucheinsicht-appeal rules: 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 seeding pass.
|
||||||
|
- Either path: editorial via `/admin/rules` (rule-editor service, Slice 11a) so the lawyer team can author + audit.
|
||||||
|
|
||||||
**Q18.1.2 — User-facing label for "appeal_target": "Worauf richtet sich die Berufung?" (DE) / "Appeal against:" (EN)?**
|
**Q18.1.2 — User-facing label for "appeal_target": "Worauf richtet sich die Berufung?" (DE) / "Appeal against:" (EN)?**
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,13 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
"deadlines.upc.disc.cfi": "Bucheinsicht",
|
"deadlines.upc.disc.cfi": "Bucheinsicht",
|
||||||
"deadlines.upc.apl.cost": "Berufung Kosten",
|
"deadlines.upc.apl.cost": "Berufung Kosten",
|
||||||
"deadlines.upc.apl.order": "Berufung Anordnungen",
|
"deadlines.upc.apl.order": "Berufung Anordnungen",
|
||||||
|
"deadlines.upc.apl": "Berufung",
|
||||||
|
"deadlines.appeal_target.label": "Worauf richtet sich die Berufung?",
|
||||||
|
"deadlines.appeal_target.endentscheidung": "Endentscheidung",
|
||||||
|
"deadlines.appeal_target.kostenentscheidung": "Kostenentscheidung",
|
||||||
|
"deadlines.appeal_target.anordnung": "Anordnung",
|
||||||
|
"deadlines.appeal_target.schadensbemessung": "Schadensbemessung",
|
||||||
|
"deadlines.appeal_target.bucheinsicht": "Bucheinsicht",
|
||||||
"deadlines.de.group.inf": "Verletzungsverfahren",
|
"deadlines.de.group.inf": "Verletzungsverfahren",
|
||||||
"deadlines.de.group.null": "Nichtigkeitsverfahren",
|
"deadlines.de.group.null": "Nichtigkeitsverfahren",
|
||||||
"deadlines.de.inf.lg": "LG (1. Instanz)",
|
"deadlines.de.inf.lg": "LG (1. Instanz)",
|
||||||
@@ -3327,6 +3334,13 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
"deadlines.upc.dmgs.cfi": "Damages Determination",
|
"deadlines.upc.dmgs.cfi": "Damages Determination",
|
||||||
"deadlines.upc.disc.cfi": "Lay-open Books",
|
"deadlines.upc.disc.cfi": "Lay-open Books",
|
||||||
"deadlines.upc.apl.cost": "Cost-Decision Appeal",
|
"deadlines.upc.apl.cost": "Cost-Decision Appeal",
|
||||||
|
"deadlines.upc.apl": "Appeal",
|
||||||
|
"deadlines.appeal_target.label": "Appeal against:",
|
||||||
|
"deadlines.appeal_target.endentscheidung": "Final Decision",
|
||||||
|
"deadlines.appeal_target.kostenentscheidung": "Cost Decision",
|
||||||
|
"deadlines.appeal_target.anordnung": "Order",
|
||||||
|
"deadlines.appeal_target.schadensbemessung": "Damages Determination",
|
||||||
|
"deadlines.appeal_target.bucheinsicht": "Lay-open Books",
|
||||||
"deadlines.upc.apl.order": "Order Appeal (15-day)",
|
"deadlines.upc.apl.order": "Order Appeal (15-day)",
|
||||||
"deadlines.de.group.inf": "Infringement proceedings",
|
"deadlines.de.group.inf": "Infringement proceedings",
|
||||||
"deadlines.de.group.null": "Nullity proceedings",
|
"deadlines.de.group.null": "Nullity proceedings",
|
||||||
|
|||||||
@@ -64,9 +64,7 @@ let sidePrefilledFromProject = false;
|
|||||||
// Conservative — false negatives just hide a control; false positives
|
// Conservative — false negatives just hide a control; false positives
|
||||||
// would show an irrelevant control.
|
// would show an irrelevant control.
|
||||||
const APPELLANT_AXIS_PROCEEDINGS = new Set([
|
const APPELLANT_AXIS_PROCEEDINGS = new Set([
|
||||||
"upc.apl.merits",
|
"upc.apl",
|
||||||
"upc.apl.cost",
|
|
||||||
"upc.apl.order",
|
|
||||||
"de.inf.olg",
|
"de.inf.olg",
|
||||||
"de.inf.bgh",
|
"de.inf.bgh",
|
||||||
"de.null.bgh",
|
"de.null.bgh",
|
||||||
@@ -75,6 +73,29 @@ const APPELLANT_AXIS_PROCEEDINGS = new Set([
|
|||||||
"epa.opp.boa",
|
"epa.opp.boa",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Slice B1 (m/paliad#124 §18.1) — Berufung unification.
|
||||||
|
// Proceedings that surface the appeal-target chip group. Currently
|
||||||
|
// only the unified upc.apl proceeding; future variants (e.g. de.apl)
|
||||||
|
// can opt in by adding the code here.
|
||||||
|
const APPEAL_TARGET_PROCEEDINGS = new Set([
|
||||||
|
"upc.apl",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Five canonical appeal-target slugs (lp.AppealTargets — keep ordered
|
||||||
|
// in sync with pkg/litigationplanner/types.go AppealTargets).
|
||||||
|
const APPEAL_TARGETS = [
|
||||||
|
"endentscheidung",
|
||||||
|
"kostenentscheidung",
|
||||||
|
"anordnung",
|
||||||
|
"schadensbemessung",
|
||||||
|
"bucheinsicht",
|
||||||
|
] as const;
|
||||||
|
type AppealTarget = (typeof APPEAL_TARGETS)[number] | "";
|
||||||
|
|
||||||
|
function hasAppealTarget(proceedingType: string): boolean {
|
||||||
|
return APPEAL_TARGET_PROCEEDINGS.has(proceedingType);
|
||||||
|
}
|
||||||
|
|
||||||
function hasAppellantAxis(proceedingType: string): boolean {
|
function hasAppellantAxis(proceedingType: string): boolean {
|
||||||
return APPELLANT_AXIS_PROCEEDINGS.has(proceedingType);
|
return APPELLANT_AXIS_PROCEEDINGS.has(proceedingType);
|
||||||
}
|
}
|
||||||
@@ -103,6 +124,32 @@ function writeAppellantToURL(a: Side) {
|
|||||||
window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
|
window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slice B1 — appeal-target URL state. Empty string = no target picked
|
||||||
|
// (the row is hidden because the proceeding isn't an appeal). Any
|
||||||
|
// other value must be one of APPEAL_TARGETS; unknown values are
|
||||||
|
// rejected by readAppealTargetFromURL so a stale link can't break
|
||||||
|
// the engine filter.
|
||||||
|
function readAppealTargetFromURL(): AppealTarget {
|
||||||
|
const raw = new URLSearchParams(window.location.search).get("target") || "";
|
||||||
|
if ((APPEAL_TARGETS as readonly string[]).includes(raw)) {
|
||||||
|
return raw as AppealTarget;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeAppealTargetToURL(t: AppealTarget) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (t === "") url.searchParams.delete("target");
|
||||||
|
else url.searchParams.set("target", t);
|
||||||
|
window.history.replaceState(null, "", url.pathname + (url.search ? url.search : "") + url.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default target on first picker entry into upc.apl. m: Endentscheidung
|
||||||
|
// is the most-common appeal target; the chip group also defaults
|
||||||
|
// "Endentscheidung" checked in verfahrensablauf.tsx. Keep these two in
|
||||||
|
// sync so the URL-less default render hits the same code path.
|
||||||
|
let currentAppealTarget: AppealTarget = "";
|
||||||
|
|
||||||
// Per-rule anchor overrides set by the click-to-edit affordance on
|
// Per-rule anchor overrides set by the click-to-edit affordance on
|
||||||
// timeline / column date cells. Posted as `anchorOverrides` to the
|
// timeline / column date cells. Posted as `anchorOverrides` to the
|
||||||
// /api/tools/fristenrechner calc so downstream rules re-anchor off the
|
// /api/tools/fristenrechner calc so downstream rules re-anchor off the
|
||||||
@@ -268,6 +315,13 @@ async function doCalc() {
|
|||||||
const overrides: Record<string, string> = {};
|
const overrides: Record<string, string> = {};
|
||||||
for (const [code, date] of anchorOverrides) overrides[code] = date;
|
for (const [code, date] of anchorOverrides) overrides[code] = date;
|
||||||
|
|
||||||
|
// Slice B1 (m/paliad#124 §18.1): for the unified upc.apl Berufung,
|
||||||
|
// default to "endentscheidung" when no chip pick is stored in URL.
|
||||||
|
// For non-appeal proceedings the engine ignores opts.AppealTarget.
|
||||||
|
const appealTarget = hasAppealTarget(selectedType)
|
||||||
|
? (currentAppealTarget || "endentscheidung")
|
||||||
|
: "";
|
||||||
|
|
||||||
const data = await calculateDeadlines({
|
const data = await calculateDeadlines({
|
||||||
proceedingType: selectedType,
|
proceedingType: selectedType,
|
||||||
triggerDate,
|
triggerDate,
|
||||||
@@ -276,6 +330,7 @@ async function doCalc() {
|
|||||||
courtId,
|
courtId,
|
||||||
perCardChoices,
|
perCardChoices,
|
||||||
includeHidden: showHidden,
|
includeHidden: showHidden,
|
||||||
|
appealTarget,
|
||||||
});
|
});
|
||||||
if (seq !== calcSeq) return;
|
if (seq !== calcSeq) return;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -447,6 +502,7 @@ function selectProceeding(btn: HTMLButtonElement) {
|
|||||||
void populateCourtPicker("court-picker-row", "court-picker", selectedType);
|
void populateCourtPicker("court-picker-row", "court-picker", selectedType);
|
||||||
syncFlagRows();
|
syncFlagRows();
|
||||||
syncAppellantRowVisibility();
|
syncAppellantRowVisibility();
|
||||||
|
syncAppealTargetRowVisibility();
|
||||||
|
|
||||||
setProceedingPickerCollapsed(true, proceedingDisplayName(btn));
|
setProceedingPickerCollapsed(true, proceedingDisplayName(btn));
|
||||||
|
|
||||||
@@ -471,6 +527,23 @@ function syncAppellantRowVisibility() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slice B1 (m/paliad#124 §18.1) — Berufung unification.
|
||||||
|
// syncAppealTargetRowVisibility shows the appeal-target chip group
|
||||||
|
// when the unified upc.apl Berufung tile is selected, hides it
|
||||||
|
// otherwise. Mirrors syncAppellantRowVisibility's pattern: clears
|
||||||
|
// state + URL when hiding so a stale ?target= can't leak.
|
||||||
|
function syncAppealTargetRowVisibility() {
|
||||||
|
const row = document.getElementById("appeal-target-row");
|
||||||
|
if (!row) return;
|
||||||
|
const visible = hasAppealTarget(selectedType);
|
||||||
|
row.style.display = visible ? "" : "none";
|
||||||
|
if (!visible && currentAppealTarget !== "") {
|
||||||
|
currentAppealTarget = "";
|
||||||
|
writeAppealTargetToURL("");
|
||||||
|
syncRadioGroup("appeal-target", "endentscheidung");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function syncRadioGroup(name: string, value: string) {
|
function syncRadioGroup(name: string, value: string) {
|
||||||
document.querySelectorAll<HTMLInputElement>(`input[type=radio][name=${name}]`).forEach((input) => {
|
document.querySelectorAll<HTMLInputElement>(`input[type=radio][name=${name}]`).forEach((input) => {
|
||||||
input.checked = input.value === value;
|
input.checked = input.value === value;
|
||||||
@@ -655,8 +728,10 @@ function initViewToggle() {
|
|||||||
function initPerspectiveControls() {
|
function initPerspectiveControls() {
|
||||||
currentSide = readSideFromURL();
|
currentSide = readSideFromURL();
|
||||||
currentAppellant = readAppellantFromURL();
|
currentAppellant = readAppellantFromURL();
|
||||||
|
currentAppealTarget = readAppealTargetFromURL();
|
||||||
syncRadioGroup("side", currentSide ?? "");
|
syncRadioGroup("side", currentSide ?? "");
|
||||||
syncRadioGroup("appellant", currentAppellant ?? "");
|
syncRadioGroup("appellant", currentAppellant ?? "");
|
||||||
|
syncRadioGroup("appeal-target", currentAppealTarget || "endentscheidung");
|
||||||
syncSideHintVisibility();
|
syncSideHintVisibility();
|
||||||
|
|
||||||
document.querySelectorAll<HTMLInputElement>("input[type=radio][name=side]").forEach((input) => {
|
document.querySelectorAll<HTMLInputElement>("input[type=radio][name=side]").forEach((input) => {
|
||||||
@@ -679,6 +754,23 @@ function initPerspectiveControls() {
|
|||||||
if (lastResponse) renderResults(lastResponse);
|
if (lastResponse) renderResults(lastResponse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Slice B1 (m/paliad#124 §18.1) — appeal-target chip handler.
|
||||||
|
// Each chip change re-fetches with the new target slug so the
|
||||||
|
// timeline re-renders against the matching rule subset.
|
||||||
|
document.querySelectorAll<HTMLInputElement>("input[type=radio][name=appeal-target]").forEach((input) => {
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
if (!input.checked) return;
|
||||||
|
const v = input.value;
|
||||||
|
if ((APPEAL_TARGETS as readonly string[]).includes(v)) {
|
||||||
|
currentAppealTarget = v as AppealTarget;
|
||||||
|
} else {
|
||||||
|
currentAppealTarget = "";
|
||||||
|
}
|
||||||
|
writeAppealTargetToURL(currentAppealTarget);
|
||||||
|
scheduleCalc(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
|||||||
@@ -195,6 +195,12 @@ export interface CalcParams {
|
|||||||
// Sent only when the page-level "Ausgeblendete anzeigen" toggle is
|
// Sent only when the page-level "Ausgeblendete anzeigen" toggle is
|
||||||
// ON.
|
// ON.
|
||||||
includeHidden?: boolean;
|
includeHidden?: boolean;
|
||||||
|
// Slice B1 / m/paliad#124 §18.1: narrows the unified UPC Berufung
|
||||||
|
// (upc.apl) timeline to the rule subset whose applies_to_target
|
||||||
|
// contains the requested slug. Empty = no filter. Valid values:
|
||||||
|
// endentscheidung | kostenentscheidung | anordnung |
|
||||||
|
// schadensbemessung | bucheinsicht.
|
||||||
|
appealTarget?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARTY_CLASS: Record<string, string> = {
|
const PARTY_CLASS: Record<string, string> = {
|
||||||
@@ -811,6 +817,7 @@ export async function calculateDeadlines(params: CalcParams): Promise<DeadlineRe
|
|||||||
? params.perCardChoices
|
? params.perCardChoices
|
||||||
: undefined,
|
: undefined,
|
||||||
includeHidden: params.includeHidden ? true : undefined,
|
includeHidden: params.includeHidden ? true : undefined,
|
||||||
|
appealTarget: params.appealTarget || undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
|
|||||||
@@ -1184,6 +1184,12 @@ export type I18nKey =
|
|||||||
| "deadlines.adjusted.weekend"
|
| "deadlines.adjusted.weekend"
|
||||||
| "deadlines.adjusted.weekend.saturday"
|
| "deadlines.adjusted.weekend.saturday"
|
||||||
| "deadlines.adjusted.weekend.sunday"
|
| "deadlines.adjusted.weekend.sunday"
|
||||||
|
| "deadlines.appeal_target.anordnung"
|
||||||
|
| "deadlines.appeal_target.bucheinsicht"
|
||||||
|
| "deadlines.appeal_target.endentscheidung"
|
||||||
|
| "deadlines.appeal_target.kostenentscheidung"
|
||||||
|
| "deadlines.appeal_target.label"
|
||||||
|
| "deadlines.appeal_target.schadensbemessung"
|
||||||
| "deadlines.appellant.claimant"
|
| "deadlines.appellant.claimant"
|
||||||
| "deadlines.appellant.defendant"
|
| "deadlines.appellant.defendant"
|
||||||
| "deadlines.appellant.label"
|
| "deadlines.appellant.label"
|
||||||
@@ -1511,6 +1517,7 @@ export type I18nKey =
|
|||||||
| "deadlines.trigger.label"
|
| "deadlines.trigger.label"
|
||||||
| "deadlines.unavailable"
|
| "deadlines.unavailable"
|
||||||
| "deadlines.upc"
|
| "deadlines.upc"
|
||||||
|
| "deadlines.upc.apl"
|
||||||
| "deadlines.upc.apl.cost"
|
| "deadlines.upc.apl.cost"
|
||||||
| "deadlines.upc.apl.merits"
|
| "deadlines.upc.apl.merits"
|
||||||
| "deadlines.upc.apl.order"
|
| "deadlines.upc.apl.order"
|
||||||
|
|||||||
@@ -28,16 +28,20 @@ function proceedingBtn(p: ProceedingDef): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slice B1 (m/paliad#124 §18.1): the 3 separate Berufung tiles
|
||||||
|
// (upc.apl.merits / upc.apl.cost / upc.apl.order) collapse into ONE
|
||||||
|
// unified "Berufung" tile (upc.apl). After picking it, the user
|
||||||
|
// selects which decision the appeal is directed AT via the
|
||||||
|
// .appeal-target-row chip group below — the engine then filters
|
||||||
|
// rules whose applies_to_target contains the picked slug.
|
||||||
const UPC_TYPES: ProceedingDef[] = [
|
const UPC_TYPES: ProceedingDef[] = [
|
||||||
{ code: "upc.inf.cfi", i18nKey: "deadlines.upc.inf.cfi", name: "Verletzungsverfahren" },
|
{ code: "upc.inf.cfi", i18nKey: "deadlines.upc.inf.cfi", name: "Verletzungsverfahren" },
|
||||||
{ code: "upc.rev.cfi", i18nKey: "deadlines.upc.rev.cfi", name: "Nichtigkeitsklage" },
|
{ code: "upc.rev.cfi", i18nKey: "deadlines.upc.rev.cfi", name: "Nichtigkeitsklage" },
|
||||||
{ code: "upc.ccr.cfi", i18nKey: "deadlines.upc.ccr.cfi", name: "Widerklage auf Nichtigkeit" },
|
{ code: "upc.ccr.cfi", i18nKey: "deadlines.upc.ccr.cfi", name: "Widerklage auf Nichtigkeit" },
|
||||||
{ code: "upc.pi.cfi", i18nKey: "deadlines.upc.pi.cfi", name: "Einstw. Maßnahmen" },
|
{ code: "upc.pi.cfi", i18nKey: "deadlines.upc.pi.cfi", name: "Einstw. Maßnahmen" },
|
||||||
{ code: "upc.apl.merits", i18nKey: "deadlines.upc.apl.merits", name: "Berufung" },
|
{ code: "upc.apl", i18nKey: "deadlines.upc.apl", name: "Berufung" },
|
||||||
{ code: "upc.dmgs.cfi", i18nKey: "deadlines.upc.dmgs.cfi", name: "Schadensbemessung" },
|
{ code: "upc.dmgs.cfi", i18nKey: "deadlines.upc.dmgs.cfi", name: "Schadensbemessung" },
|
||||||
{ code: "upc.disc.cfi", i18nKey: "deadlines.upc.disc.cfi", name: "Bucheinsicht" },
|
{ code: "upc.disc.cfi", i18nKey: "deadlines.upc.disc.cfi", name: "Bucheinsicht" },
|
||||||
{ code: "upc.apl.cost", i18nKey: "deadlines.upc.apl.cost", name: "Berufung Kosten" },
|
|
||||||
{ code: "upc.apl.order", i18nKey: "deadlines.upc.apl.order", name: "Berufung Anordnungen" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// DE proceedings split by type (Verletzung / Nichtigkeit) per m's
|
// DE proceedings split by type (Verletzung / Nichtigkeit) per m's
|
||||||
@@ -216,6 +220,36 @@ export function renderVerfahrensablauf(): string {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Appeal-target chip row (Slice B1 / m/paliad#124 §18.1).
|
||||||
|
Shown only when the unified upc.apl Berufung tile is
|
||||||
|
selected; lets the user narrow the timeline to the
|
||||||
|
rules whose applies_to_target contains the picked
|
||||||
|
decision kind. URL state ?target=<slug>. */}
|
||||||
|
<div className="verfahrensablauf-perspective-row" id="appeal-target-row" style="display:none">
|
||||||
|
<span className="date-label" data-i18n="deadlines.appeal_target.label">Worauf richtet sich die Berufung?</span>
|
||||||
|
<div className="fristen-view-toggle" role="radiogroup" aria-label="Appeal target">
|
||||||
|
<label className="fristen-view-option">
|
||||||
|
<input type="radio" name="appeal-target" value="endentscheidung" checked />
|
||||||
|
<span data-i18n="deadlines.appeal_target.endentscheidung">Endentscheidung</span>
|
||||||
|
</label>
|
||||||
|
<label className="fristen-view-option">
|
||||||
|
<input type="radio" name="appeal-target" value="kostenentscheidung" />
|
||||||
|
<span data-i18n="deadlines.appeal_target.kostenentscheidung">Kostenentscheidung</span>
|
||||||
|
</label>
|
||||||
|
<label className="fristen-view-option">
|
||||||
|
<input type="radio" name="appeal-target" value="anordnung" />
|
||||||
|
<span data-i18n="deadlines.appeal_target.anordnung">Anordnung</span>
|
||||||
|
</label>
|
||||||
|
<label className="fristen-view-option">
|
||||||
|
<input type="radio" name="appeal-target" value="schadensbemessung" />
|
||||||
|
<span data-i18n="deadlines.appeal_target.schadensbemessung">Schadensbemessung</span>
|
||||||
|
</label>
|
||||||
|
<label className="fristen-view-option">
|
||||||
|
<input type="radio" name="appeal-target" value="bucheinsicht" />
|
||||||
|
<span data-i18n="deadlines.appeal_target.bucheinsicht">Bucheinsicht</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="verfahrensablauf-perspective-row" id="appellant-row" style="display:none">
|
<div className="verfahrensablauf-perspective-row" id="appellant-row" style="display:none">
|
||||||
<span className="date-label" data-i18n="deadlines.appellant.label">Berufung durch:</span>
|
<span className="date-label" data-i18n="deadlines.appellant.label">Berufung durch:</span>
|
||||||
<div className="fristen-view-toggle" role="radiogroup" aria-label="Appellant">
|
<div className="fristen-view-toggle" role="radiogroup" aria-label="Appellant">
|
||||||
|
|||||||
63
internal/db/migrations/134_berufung_unification.down.sql
Normal file
63
internal/db/migrations/134_berufung_unification.down.sql
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
-- 134_berufung_unification — DOWN
|
||||||
|
--
|
||||||
|
-- Reverses the Berufung unification: un-archives the 3 old appeal
|
||||||
|
-- proceeding_types, points the 16 rules back at their original
|
||||||
|
-- proceeding by their applies_to_target stamp, drops the new
|
||||||
|
-- upc.apl row, drops the two columns + their CHECK constraints.
|
||||||
|
--
|
||||||
|
-- The 3 old proceeding_types are recovered by code (we archived them,
|
||||||
|
-- never deleted them — that's what makes this down-migration safe).
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- 1. Un-archive the 3 old appeal proceeding_types.
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
|
||||||
|
UPDATE paliad.proceeding_types
|
||||||
|
SET is_active = true,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE code IN ('upc.apl.merits', 'upc.apl.cost', 'upc.apl.order');
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- 2. Point rules back at their original proceeding_type by stamp.
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
|
||||||
|
UPDATE paliad.deadline_rules dr
|
||||||
|
SET proceeding_type_id = (
|
||||||
|
SELECT id FROM paliad.proceeding_types WHERE code = 'upc.apl.merits'
|
||||||
|
)
|
||||||
|
WHERE dr.applies_to_target = ARRAY['endentscheidung']::text[];
|
||||||
|
|
||||||
|
UPDATE paliad.deadline_rules dr
|
||||||
|
SET proceeding_type_id = (
|
||||||
|
SELECT id FROM paliad.proceeding_types WHERE code = 'upc.apl.cost'
|
||||||
|
)
|
||||||
|
WHERE dr.applies_to_target = ARRAY['kostenentscheidung']::text[];
|
||||||
|
|
||||||
|
UPDATE paliad.deadline_rules dr
|
||||||
|
SET proceeding_type_id = (
|
||||||
|
SELECT id FROM paliad.proceeding_types WHERE code = 'upc.apl.order'
|
||||||
|
)
|
||||||
|
WHERE dr.applies_to_target = ARRAY['anordnung']::text[];
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- 3. Drop the unified upc.apl row (now orphaned).
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
|
||||||
|
DELETE FROM paliad.proceeding_types WHERE code = 'upc.apl';
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
-- 4. Drop the new columns + their CHECK constraints.
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
|
|
||||||
|
ALTER TABLE paliad.deadline_rules
|
||||||
|
DROP CONSTRAINT IF EXISTS deadline_rules_applies_to_target_chk;
|
||||||
|
|
||||||
|
ALTER TABLE paliad.deadline_rules
|
||||||
|
DROP COLUMN IF EXISTS applies_to_target;
|
||||||
|
|
||||||
|
ALTER TABLE paliad.proceeding_types
|
||||||
|
DROP CONSTRAINT IF EXISTS proceeding_types_appeal_target_chk;
|
||||||
|
|
||||||
|
ALTER TABLE paliad.proceeding_types
|
||||||
|
DROP COLUMN IF EXISTS appeal_target;
|
||||||
263
internal/db/migrations/134_berufung_unification.up.sql
Normal file
263
internal/db/migrations/134_berufung_unification.up.sql
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
-- 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.
|
||||||
|
-- ---------------------------------------------------------------
|
||||||
@@ -69,6 +69,14 @@ func handleFristenrechnerAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
// stay in the result list. Default false preserves the legacy
|
// stay in the result list. Default false preserves the legacy
|
||||||
// suppression. HiddenCount on the response is independent.
|
// suppression. HiddenCount on the response is independent.
|
||||||
IncludeHidden bool `json:"includeHidden,omitempty"`
|
IncludeHidden bool `json:"includeHidden,omitempty"`
|
||||||
|
// Slice B1 / m/paliad#124 §18.1: narrows the unified UPC
|
||||||
|
// Berufung (upc.apl) timeline to the rule subset whose
|
||||||
|
// applies_to_target contains the requested slug. Empty = no
|
||||||
|
// filter. Valid values: endentscheidung | kostenentscheidung
|
||||||
|
// | anordnung | schadensbemessung | bucheinsicht. Unknown
|
||||||
|
// slugs are silently dropped (no filter) so a stale frontend
|
||||||
|
// chip doesn't 400 the request.
|
||||||
|
AppealTarget string `json:"appealTarget,omitempty"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage"})
|
||||||
@@ -116,6 +124,7 @@ func handleFristenrechnerAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
SkipRules: addendum.SkipRules,
|
SkipRules: addendum.SkipRules,
|
||||||
IncludeCCRFor: addendum.IncludeCCRFor,
|
IncludeCCRFor: addendum.IncludeCCRFor,
|
||||||
IncludeHidden: req.IncludeHidden,
|
IncludeHidden: req.IncludeHidden,
|
||||||
|
AppealTarget: req.AppealTarget,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, services.ErrUnknownProceedingType) {
|
if errors.Is(err, services.ErrUnknownProceedingType) {
|
||||||
|
|||||||
@@ -35,11 +35,12 @@ const ruleColumns = `id, proceeding_type_id, parent_id, submission_code, name, n
|
|||||||
created_at, updated_at,
|
created_at, updated_at,
|
||||||
trigger_event_id, spawn_proceeding_type_id, combine_op, condition_expr,
|
trigger_event_id, spawn_proceeding_type_id, combine_op, condition_expr,
|
||||||
priority, is_court_set, lifecycle_state, draft_of, published_at,
|
priority, is_court_set, lifecycle_state, draft_of, published_at,
|
||||||
choices_offered`
|
choices_offered, applies_to_target`
|
||||||
|
|
||||||
const proceedingTypeColumns = `id, code, name, name_en, description, jurisdiction,
|
const proceedingTypeColumns = `id, code, name, name_en, description, jurisdiction,
|
||||||
category, default_color, sort_order, is_active,
|
category, default_color, sort_order, is_active,
|
||||||
trigger_event_label_de, trigger_event_label_en`
|
trigger_event_label_de, trigger_event_label_en,
|
||||||
|
appeal_target`
|
||||||
|
|
||||||
// List returns active rules, optionally filtered by proceeding type.
|
// List returns active rules, optionally filtered by proceeding type.
|
||||||
// Each row has ConceptDefaultEventTypeID hydrated from
|
// Each row has ConceptDefaultEventTypeID hydrated from
|
||||||
|
|||||||
@@ -126,6 +126,25 @@ func Calculate(
|
|||||||
rules = ApplyRuleOverrides(rules, opts.RuleOverrides)
|
rules = ApplyRuleOverrides(rules, opts.RuleOverrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppealTarget filter (Slice B1, m/paliad#124 §18.1). When set,
|
||||||
|
// keep only rules whose AppliesToTarget contains the requested
|
||||||
|
// slug. Unknown slugs short-circuit to no-op (defensive: a stale
|
||||||
|
// frontend chip shouldn't break the render). Empty AppliesToTarget
|
||||||
|
// on a rule means "doesn't belong to an appeal target" — such a
|
||||||
|
// rule is suppressed under any non-empty AppealTarget filter.
|
||||||
|
if opts.AppealTarget != "" && IsValidAppealTarget(opts.AppealTarget) {
|
||||||
|
filtered := make([]Rule, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
for _, t := range r.AppliesToTarget {
|
||||||
|
if t == opts.AppealTarget {
|
||||||
|
filtered = append(filtered, r)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rules = filtered
|
||||||
|
}
|
||||||
|
|
||||||
// ruleByID lets the conditional-rendering branches resolve a parent
|
// ruleByID lets the conditional-rendering branches resolve a parent
|
||||||
// rule's display fields (submission_code, name, name_en) for the
|
// rule's display fields (submission_code, name, name_en) for the
|
||||||
// "abhängig von <ParentRuleName>" chip without re-scanning the rules
|
// "abhängig von <ParentRuleName>" chip without re-scanning the rules
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NullableJSON is a jsonb column that may be NULL. json.RawMessage
|
// NullableJSON is a jsonb column that may be NULL. json.RawMessage
|
||||||
@@ -149,6 +150,13 @@ type Rule struct {
|
|||||||
// rule offers on the Verfahrensablauf timeline (mig 129,
|
// rule offers on the Verfahrensablauf timeline (mig 129,
|
||||||
// t-paliad-265). NULL = no caret affordance (default).
|
// t-paliad-265). NULL = no caret affordance (default).
|
||||||
ChoicesOffered NullableJSON `db:"choices_offered" json:"choices_offered,omitempty"`
|
ChoicesOffered NullableJSON `db:"choices_offered" json:"choices_offered,omitempty"`
|
||||||
|
|
||||||
|
// AppliesToTarget is the per-rule applies-to set for the unified
|
||||||
|
// UPC Berufung proceeding type (Slice B1, mig 134, m/paliad#124
|
||||||
|
// §18.1). Each element ∈ AppealTargets. NULL on rules outside
|
||||||
|
// the appeal proceeding. The engine filters by this when
|
||||||
|
// CalcOptions.AppealTarget is set.
|
||||||
|
AppliesToTarget pq.StringArray `db:"applies_to_target" json:"appliesToTarget,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProceedingType is one of the litigation conceptual codes (INF/REV/CCR
|
// ProceedingType is one of the litigation conceptual codes (INF/REV/CCR
|
||||||
@@ -171,6 +179,12 @@ type ProceedingType struct {
|
|||||||
// that fires when no rule has IsRootEvent=true.
|
// that fires when no rule has IsRootEvent=true.
|
||||||
TriggerEventLabelDE *string `db:"trigger_event_label_de" json:"trigger_event_label_de,omitempty"`
|
TriggerEventLabelDE *string `db:"trigger_event_label_de" json:"trigger_event_label_de,omitempty"`
|
||||||
TriggerEventLabelEN *string `db:"trigger_event_label_en" json:"trigger_event_label_en,omitempty"`
|
TriggerEventLabelEN *string `db:"trigger_event_label_en" json:"trigger_event_label_en,omitempty"`
|
||||||
|
|
||||||
|
// AppealTarget is the top-level appeal-target marker (Slice B1, mig
|
||||||
|
// 134). NULL on non-appeal proceedings. Reserved for future variants
|
||||||
|
// — today the unified upc.apl row has this NULL (per-rule targets
|
||||||
|
// live on Rule.AppliesToTarget).
|
||||||
|
AppealTarget *string `db:"appeal_target" json:"appeal_target,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdjustmentReason describes why a date was rolled forward / backward
|
// AdjustmentReason describes why a date was rolled forward / backward
|
||||||
@@ -253,6 +267,14 @@ type CalcOptions struct {
|
|||||||
IncludeHidden bool
|
IncludeHidden bool
|
||||||
|
|
||||||
ProjectHint ProjectHint
|
ProjectHint ProjectHint
|
||||||
|
|
||||||
|
// AppealTarget narrows the timeline to rules whose AppliesToTarget
|
||||||
|
// contains the requested slug. Empty = no filter. Set to one of
|
||||||
|
// AppealTargets for the unified UPC Berufung picker (Slice B1,
|
||||||
|
// m/paliad#124 §18.1). Unknown slugs are silently dropped (no
|
||||||
|
// filter applied) so a stale frontend chip doesn't break the
|
||||||
|
// timeline render — see IsValidAppealTarget.
|
||||||
|
AppealTarget string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectHint scopes a Catalog call to a specific project. Paliad's
|
// ProjectHint scopes a Catalog call to a specific project. Paliad's
|
||||||
@@ -426,3 +448,52 @@ var (
|
|||||||
ErrUnknownProceedingType = errors.New("unknown proceeding type")
|
ErrUnknownProceedingType = errors.New("unknown proceeding type")
|
||||||
ErrUnknownRule = errors.New("unknown rule")
|
ErrUnknownRule = errors.New("unknown rule")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AppealTarget* are the canonical slugs for the unified UPC Berufung
|
||||||
|
// proceeding type's appeal-target discriminator (Slice B1, m/paliad#124
|
||||||
|
// §18.1). The verfahrensablauf picker renders one "Berufung" entry;
|
||||||
|
// the user then picks one of these five targets and the engine filters
|
||||||
|
// rules whose AppliesToTarget contains the requested slug.
|
||||||
|
//
|
||||||
|
// Schadensbemessung + Bucheinsicht have no rule rows in migration 134;
|
||||||
|
// per m's 2026-05-26 decision they are distinct from the merits track
|
||||||
|
// and their rule sets will be seeded in a follow-up slice (paired with
|
||||||
|
// t-paliad-193 orphan-concept-seed or editorial via /admin/rules).
|
||||||
|
// CalcOptions.AppealTarget="schadensbemessung" or "bucheinsicht"
|
||||||
|
// currently returns an empty timeline.
|
||||||
|
const (
|
||||||
|
AppealTargetEndentscheidung = "endentscheidung"
|
||||||
|
AppealTargetKostenentscheidung = "kostenentscheidung"
|
||||||
|
AppealTargetAnordnung = "anordnung"
|
||||||
|
AppealTargetSchadensbemessung = "schadensbemessung"
|
||||||
|
AppealTargetBucheinsicht = "bucheinsicht"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppealTargets is the canonical ordered list for UI chip rendering +
|
||||||
|
// validation. Order matches the design doc + the frontend's i18n key
|
||||||
|
// ordering — do not reorder without coordinating with the chip-group
|
||||||
|
// renderer.
|
||||||
|
var AppealTargets = []string{
|
||||||
|
AppealTargetEndentscheidung,
|
||||||
|
AppealTargetKostenentscheidung,
|
||||||
|
AppealTargetAnordnung,
|
||||||
|
AppealTargetSchadensbemessung,
|
||||||
|
AppealTargetBucheinsicht,
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidAppealTarget returns true for empty (no filter requested) or
|
||||||
|
// any of the five canonical slugs. The engine uses this to gate the
|
||||||
|
// CalcOptions.AppealTarget filter — an unknown slug is silently
|
||||||
|
// dropped (no filter applied) rather than producing an error, so a
|
||||||
|
// stale frontend chip doesn't break the timeline render.
|
||||||
|
func IsValidAppealTarget(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, t := range AppealTargets {
|
||||||
|
if t == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user