After Slice B1's Berufung unification (mig 134), the picker exposed five appeal targets but only three carried rules. Schadensbemessung and Bucheinsicht returned empty timelines. m's 2026-05-26 decision (#134): R.224 is uniform across substantive R.118 decisions, and R.220.2 / R.224.2.b / R.235.2 / R.237 / R.238.2 are uniform across the orders they appeal — so the existing merits-track and order-track rules can carry the missing targets via a non-destructive applies_to_target extension. Audit of live `paliad.deadline_rules` for upc.apl.unified (proceeding_type_id=160): - 7 endentscheidung rules → extend with 'schadensbemessung' - 7 anordnung rules → extend with 'bucheinsicht' - 2 kostenentscheidung rules — untouched (distinct leave-to-appeal track) Migration: - set_config('paliad.audit_reason', …) at top of UP and DOWN — required by the mig 079 deadline_rule_audit_trigger on every UPDATE. - Audit-first DO block lists every row to be touched (pre/post state) and RAISE EXCEPTIONs on pre-condition drift (missing proceeding_type, wrong rule counts, partial-run carry-over of the new targets). - Two narrow UPDATEs keyed off upc.apl.unified + existing target + absence of new target. - Post-sanity asserts schad=7, buch=7, end=7, anord=7, cost=2 — hard RAISE EXCEPTION on any drift. - DOWN strips both new targets via array_remove with the same WHERE. - No deadline_rules.updated_at writes; column exists but the migration is single-purpose and leaves it as-is. Dry-run via Supabase MCP confirmed: - UP yields {schad:7, buch:7, end:7, anord:7, cost:2} on prod. - DOWN restores {schad:0, buch:0, end:7, anord:7, cost:2}. - DB returned to pre-state; the real golang-migrate boot path will apply 138 cleanly at next deploy. Version bump 137→138: cronus's mig 137 (proceeding_role_labels, #132) merged to main while this branch was in flight. Rebased onto current main, renamed files, rewrote all "mig 137" references inside the SQL + test code. Test: - lookup_events_test.go: the schadensbemessung empty-result assertion becomes the inverse (rules expected). Adds a parallel bucheinsicht assertion. Same anchor-row shape check as the existing endentscheidung case (DepthFromAnchor=1, target ∈ AppliesToTarget, proceeding_type = upc.apl.unified). - `go test ./...` green post-rebase, including pkg/litigationplanner/ appeal_target_label_test.go added by cronus's mig 137. Refs: m/paliad#134, t-paliad-303. Lessons applied from mig 134 hotfixes: audit_reason set_config, no updated_at writes, audit live DB before drafting, RAISE EXCEPTION on integrity violations.
220 lines
7.0 KiB
Go
220 lines
7.0 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/lib/pq"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/db"
|
|
lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
|
|
)
|
|
|
|
// TestLookupEvents covers the multi-axis catalog query API from Slice
|
|
// B2 (m/paliad#124 §18.2). Skipped when TEST_DATABASE_URL is unset,
|
|
// mirroring TestCalculateRule.
|
|
//
|
|
// Cases:
|
|
// - jurisdiction=UPC, depth=all-following → every active+published
|
|
// UPC rule, anchor depth=1 for all (no parent_id outside the
|
|
// filtered set lights up depth>1 because the entire UPC subset is
|
|
// a single anchor cohort).
|
|
// - proceeding_type_id (upc.inf.cfi) + party=defendant + depth=next
|
|
// → defendant rules in upc.inf.cfi at depth=1 + direct children
|
|
// of those at depth=2.
|
|
// - unknown jurisdiction value → silently ignored, no filter applied.
|
|
// - empty axes → all rules (no filter on any axis).
|
|
func TestLookupEvents(t *testing.T) {
|
|
url := os.Getenv("TEST_DATABASE_URL")
|
|
if url == "" {
|
|
t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
|
|
}
|
|
if err := db.ApplyMigrations(url); err != nil {
|
|
t.Fatalf("apply migrations: %v", err)
|
|
}
|
|
pool, err := sqlx.Connect("postgres", url)
|
|
if err != nil {
|
|
t.Fatalf("connect: %v", err)
|
|
}
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
rules := NewDeadlineRuleService(pool)
|
|
catalog := &paliadCatalog{rules: rules}
|
|
|
|
t.Run("jurisdiction=UPC, all-following returns the UPC corpus", func(t *testing.T) {
|
|
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
|
|
Jurisdiction: "UPC",
|
|
}, lp.EventLookupDepthAllFollowing)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents: %v", err)
|
|
}
|
|
if len(matches) == 0 {
|
|
t.Fatal("expected non-empty UPC corpus")
|
|
}
|
|
// Every match must be a UPC rule.
|
|
for _, m := range matches {
|
|
if m.ProceedingType.Jurisdiction == nil || *m.ProceedingType.Jurisdiction != "UPC" {
|
|
t.Errorf("non-UPC row leaked into UPC-axis query: code=%s jurisdiction=%v",
|
|
m.ProceedingType.Code, m.ProceedingType.Jurisdiction)
|
|
}
|
|
if m.DepthFromAnchor < 1 {
|
|
t.Errorf("depth=%d for rule %s, want >= 1", m.DepthFromAnchor, m.Rule.ID)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("party=defendant scopes to defendant rules", func(t *testing.T) {
|
|
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
|
|
Jurisdiction: "UPC",
|
|
Party: "defendant",
|
|
}, lp.EventLookupDepthNext)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents: %v", err)
|
|
}
|
|
if len(matches) == 0 {
|
|
t.Fatal("expected at least one defendant rule across the UPC corpus")
|
|
}
|
|
// Anchor matches (depth=1) must be primary_party=defendant.
|
|
// Depth=2 children appear under EventLookupDepthNext only as
|
|
// expansion from anchors — they may carry any party.
|
|
for _, m := range matches {
|
|
if m.DepthFromAnchor != 1 {
|
|
continue
|
|
}
|
|
if m.Rule.PrimaryParty == nil || *m.Rule.PrimaryParty != "defendant" {
|
|
t.Errorf("anchor row %s (depth=1) is not defendant: %v",
|
|
m.Rule.Name, m.Rule.PrimaryParty)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("unknown jurisdiction value silently falls through", func(t *testing.T) {
|
|
matchesAll, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{},
|
|
lp.EventLookupDepthAllFollowing)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents (all): %v", err)
|
|
}
|
|
matchesUnknown, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
|
|
Jurisdiction: "XX-not-a-real-jurisdiction",
|
|
}, lp.EventLookupDepthAllFollowing)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents (unknown): %v", err)
|
|
}
|
|
if len(matchesAll) != len(matchesUnknown) {
|
|
t.Errorf("unknown jurisdiction should fall through to no-filter; got %d vs all-axes %d",
|
|
len(matchesUnknown), len(matchesAll))
|
|
}
|
|
})
|
|
|
|
t.Run("appeal_target=endentscheidung returns upc.apl merits rules", func(t *testing.T) {
|
|
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
|
|
Jurisdiction: "UPC",
|
|
AppealTarget: lp.AppealTargetEndentscheidung,
|
|
}, lp.EventLookupDepthAllFollowing)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents: %v", err)
|
|
}
|
|
// Should hit the 7 rules under the unified upc.apl that
|
|
// carry applies_to_target={endentscheidung} (Slice B1 mig 134).
|
|
if len(matches) == 0 {
|
|
t.Fatal("expected upc.apl endentscheidung rules after B1 mig")
|
|
}
|
|
for _, m := range matches {
|
|
if m.DepthFromAnchor != 1 {
|
|
continue // children of anchors may be from other targets
|
|
}
|
|
found := false
|
|
for _, t := range m.Rule.AppliesToTarget {
|
|
if t == lp.AppealTargetEndentscheidung {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("anchor row %s missing endentscheidung target: %v",
|
|
m.Rule.Name, m.Rule.AppliesToTarget)
|
|
}
|
|
if m.ProceedingType.Code != "upc.apl.unified" {
|
|
t.Errorf("anchor row %s came from %s, want upc.apl.unified",
|
|
m.Rule.Name, m.ProceedingType.Code)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("appeal_target=schadensbemessung returns upc.apl merits rules (mig 138 backfill)", func(t *testing.T) {
|
|
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
|
|
Jurisdiction: "UPC",
|
|
AppealTarget: lp.AppealTargetSchadensbemessung,
|
|
}, lp.EventLookupDepthAllFollowing)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents: %v", err)
|
|
}
|
|
// mig 138 (t-paliad-303, m/paliad#134) extends the 7 merits-track
|
|
// rules under upc.apl.unified with applies_to_target ⊇ {schadensbemessung}
|
|
// because R.224 is uniform across substantive R.118 decisions.
|
|
if len(matches) == 0 {
|
|
t.Fatal("expected upc.apl schadensbemessung rules after mig 138 backfill")
|
|
}
|
|
for _, m := range matches {
|
|
if m.DepthFromAnchor != 1 {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, t := range m.Rule.AppliesToTarget {
|
|
if t == lp.AppealTargetSchadensbemessung {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("anchor row %s missing schadensbemessung target: %v",
|
|
m.Rule.Name, m.Rule.AppliesToTarget)
|
|
}
|
|
if m.ProceedingType.Code != "upc.apl.unified" {
|
|
t.Errorf("anchor row %s came from %s, want upc.apl.unified",
|
|
m.Rule.Name, m.ProceedingType.Code)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("appeal_target=bucheinsicht returns upc.apl order rules (mig 138 backfill)", func(t *testing.T) {
|
|
matches, err := catalog.LookupEvents(ctx, lp.EventLookupAxes{
|
|
Jurisdiction: "UPC",
|
|
AppealTarget: lp.AppealTargetBucheinsicht,
|
|
}, lp.EventLookupDepthAllFollowing)
|
|
if err != nil {
|
|
t.Fatalf("LookupEvents: %v", err)
|
|
}
|
|
// mig 138 (t-paliad-303, m/paliad#134) extends the 7 order-track
|
|
// rules under upc.apl.unified with applies_to_target ⊇ {bucheinsicht}
|
|
// because R.220.2 / R.224.2.b / R.235.2 / R.237 / R.238.2 are
|
|
// uniform across the orders they appeal.
|
|
if len(matches) == 0 {
|
|
t.Fatal("expected upc.apl bucheinsicht rules after mig 138 backfill")
|
|
}
|
|
for _, m := range matches {
|
|
if m.DepthFromAnchor != 1 {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, t := range m.Rule.AppliesToTarget {
|
|
if t == lp.AppealTargetBucheinsicht {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("anchor row %s missing bucheinsicht target: %v",
|
|
m.Rule.Name, m.Rule.AppliesToTarget)
|
|
}
|
|
if m.ProceedingType.Code != "upc.apl.unified" {
|
|
t.Errorf("anchor row %s came from %s, want upc.apl.unified",
|
|
m.Rule.Name, m.ProceedingType.Code)
|
|
}
|
|
}
|
|
})
|
|
}
|