m's 2026-05-20 14:08 reports on /tools/verfahrensablauf:
1. "There seems to be a lacking english term here" — picking
UPC CCR shows "Trigger event: Widerklage auf Nichtigkeit" on EN.
2. "Nothing shows in the roadmap" — the timeline is empty because
upc.ccr.cfi has no native rules (it's an illustrative peer that
normally runs as a sub-track of upc.inf.cfi with with_ccr).
Root cause for (1): UIResponse.proceedingName was DE-only. When a
proceeding had no root rule the frontend fell back to that field, so
EN users saw the DE label. The DB already has bilingual names; this
was pure plumbing.
Root cause for (2): the upc.ccr.cfi proceeding-type row exists for
the picker (mig 096) but ResolveCounterclaimRouting — the helper
that maps it to upc.inf.cfi with the with_ccr flag — was defined
but never called. Calculate queried rules directly off upc.ccr.cfi
and got an empty list.
Fix:
* Add ProceedingNameEN, ContextualNote, ContextualNoteEN to
UIResponse. Frontend triggerEventLabelFor now consults the EN
name on EN, falling back to DE only if the EN field is empty.
* New SubTrackRouting registry in proceeding_mapping.go and a
LookupSubTrackRouting lookup — single source of truth for the
"this proceeding has no native rules, route to a parent with
flags + show a contextual note" pattern. Today's only entry is
upc.ccr.cfi → upc.inf.cfi + with_ccr; the pattern generalises
to other sub-tracks via data-only additions.
* Calculate consults the registry at the top: when a hit, the
proceeding type is re-resolved to the parent for rule lookup, the
default flags are merged into the user's flag set (user flags win
on conflict), and the response identity (Code/Name/NameEN) stays
on the user-picked proceeding so the page header still reads
"Counterclaim for Revocation". The bilingual note surfaces in
ContextualNote{,EN}.
* Frontend renderResults paints a lime-accent banner above the
timeline body when the response carries a note
(.timeline-context-note). escHtml already exported from
views/verfahrensablauf-core — imported here for the banner.
No DB migration: SELECTs against paliad.proceeding_types,
paliad.deadline_rules, and paliad.trigger_events confirm every
active row already has a non-empty name_en / name. The bug was
the API + frontend never reading the EN columns through the
proceedingName fallback path.
Tests: TestSubTrackRoutings pins the registry shape (every entry
has matching key/value, non-empty parent+flags, bilingual notes;
CCR's exact shape is asserted; non-sub-tracks miss). The existing
TestResolveCounterclaimRouting continues to pass because the
helper now consults the registry but the CCR semantics are
unchanged.
124 lines
4.7 KiB
Go
124 lines
4.7 KiB
Go
package services
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestMapLitigationToFristenrechner(t *testing.T) {
|
|
type tc struct {
|
|
litigation, jurisdiction string
|
|
wantCode string
|
|
wantFlags []string
|
|
wantOK bool
|
|
}
|
|
cases := []tc{
|
|
// Unambiguous UPC fold-ins.
|
|
{"INF", "UPC", CodeUPCInfringement, nil, true},
|
|
{"REV", "UPC", CodeUPCRevocation, nil, true},
|
|
{"APP", "UPC", CodeUPCAppealMerits, nil, true},
|
|
{"APM", "UPC", CodeUPCPreliminary, nil, true},
|
|
// CCR + UPC = upc.inf.cfi with the with_ccr flag.
|
|
{"CCR", "UPC", CodeUPCInfringement, []string{"with_ccr"}, true},
|
|
// AMD + UPC = upc.inf.cfi with the with_amend flag.
|
|
{"AMD", "UPC", CodeUPCInfringement, []string{"with_amend"}, true},
|
|
// DE first-instance / Nichtigkeit mappings.
|
|
{"INF", "DE", CodeDEInfringementLG, nil, true},
|
|
{"REV", "DE", CodeDENullityBPatG, nil, true},
|
|
{"CCR", "DE", CodeDENullityBPatG, nil, true},
|
|
// EPA opposition.
|
|
{"OPP", "EPA", CodeEPAOpposition, nil, true},
|
|
// Ambiguous: APP+DE has both OLG and BGH analogues; project
|
|
// model can't disambiguate, so degrade.
|
|
{"APP", "DE", "", nil, false},
|
|
// No analogue: ZPO_CIVIL → nothing in fristenrechner.
|
|
{"ZPO_CIVIL", "DE", "", nil, false},
|
|
// AMD only fires on UPC; DE has no analogue.
|
|
{"AMD", "DE", "", nil, false},
|
|
// APM only fires on UPC.
|
|
{"APM", "EPA", "", nil, false},
|
|
// Unknown codes / jurisdictions → ok=false.
|
|
{"XXX", "UPC", "", nil, false},
|
|
{"INF", "ZZZ", "", nil, false},
|
|
{"", "", "", nil, false},
|
|
}
|
|
for _, c := range cases {
|
|
gotCode, gotFlags, gotOK := MapLitigationToFristenrechner(c.litigation, c.jurisdiction)
|
|
if gotCode != c.wantCode || gotOK != c.wantOK || !reflect.DeepEqual(gotFlags, c.wantFlags) {
|
|
t.Errorf("MapLitigationToFristenrechner(%q, %q) = (%q, %v, %v); want (%q, %v, %v)",
|
|
c.litigation, c.jurisdiction,
|
|
gotCode, gotFlags, gotOK,
|
|
c.wantCode, c.wantFlags, c.wantOK)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveCounterclaimRouting(t *testing.T) {
|
|
t.Run("upc.ccr.cfi routes to upc.inf.cfi with with_ccr", func(t *testing.T) {
|
|
gotCode, gotFlags, routed := ResolveCounterclaimRouting(CodeUPCCounterclaim)
|
|
if gotCode != CodeUPCInfringement {
|
|
t.Errorf("effective code = %q, want %q", gotCode, CodeUPCInfringement)
|
|
}
|
|
if !reflect.DeepEqual(gotFlags, []string{"with_ccr"}) {
|
|
t.Errorf("default flags = %v, want [with_ccr]", gotFlags)
|
|
}
|
|
if !routed {
|
|
t.Errorf("routed = false, want true")
|
|
}
|
|
})
|
|
t.Run("non-ccr code passes through unchanged", func(t *testing.T) {
|
|
for _, code := range []string{CodeUPCInfringement, CodeUPCRevocation, CodeDEInfringementLG, "anything-else"} {
|
|
gotCode, gotFlags, routed := ResolveCounterclaimRouting(code)
|
|
if gotCode != code {
|
|
t.Errorf("ResolveCounterclaimRouting(%q) returned %q, want pass-through", code, gotCode)
|
|
}
|
|
if gotFlags != nil {
|
|
t.Errorf("ResolveCounterclaimRouting(%q) flags = %v, want nil", code, gotFlags)
|
|
}
|
|
if routed {
|
|
t.Errorf("ResolveCounterclaimRouting(%q) routed = true, want false", code)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestSubTrackRoutings asserts the registry shape m/paliad#58 depends
|
|
// on: every entry's Code matches its map key, has a non-empty
|
|
// ParentCode + DefaultFlags + bilingual notes. Drift here silently
|
|
// breaks the spawn-as-standalone renderer (a CCR pick would 404 or
|
|
// render an empty timeline), so we pin the contract.
|
|
func TestSubTrackRoutings(t *testing.T) {
|
|
if len(SubTrackRoutings) == 0 {
|
|
t.Fatal("SubTrackRoutings is empty — at minimum upc.ccr.cfi must be registered")
|
|
}
|
|
for key, route := range SubTrackRoutings {
|
|
if route.Code != key {
|
|
t.Errorf("SubTrackRoutings[%q].Code = %q, want %q (key/value mismatch)", key, route.Code, key)
|
|
}
|
|
if route.ParentCode == "" {
|
|
t.Errorf("SubTrackRoutings[%q] has empty ParentCode", key)
|
|
}
|
|
if len(route.DefaultFlags) == 0 {
|
|
t.Errorf("SubTrackRoutings[%q] has no DefaultFlags — sub-track routing without flags is a no-op", key)
|
|
}
|
|
if route.NoteDE == "" || route.NoteEN == "" {
|
|
t.Errorf("SubTrackRoutings[%q] missing bilingual note: DE=%q EN=%q", key, route.NoteDE, route.NoteEN)
|
|
}
|
|
}
|
|
// CCR is the canonical entry — assert its exact shape so a future
|
|
// rename doesn't silently change semantics.
|
|
ccr, ok := LookupSubTrackRouting(CodeUPCCounterclaim)
|
|
if !ok {
|
|
t.Fatal("LookupSubTrackRouting(upc.ccr.cfi) returned ok=false; entry must be registered")
|
|
}
|
|
if ccr.ParentCode != CodeUPCInfringement {
|
|
t.Errorf("CCR.ParentCode = %q, want %q", ccr.ParentCode, CodeUPCInfringement)
|
|
}
|
|
if !reflect.DeepEqual(ccr.DefaultFlags, []string{"with_ccr"}) {
|
|
t.Errorf("CCR.DefaultFlags = %v, want [with_ccr]", ccr.DefaultFlags)
|
|
}
|
|
if _, miss := LookupSubTrackRouting(CodeUPCInfringement); miss {
|
|
t.Error("LookupSubTrackRouting(upc.inf.cfi) returned ok=true; non-sub-track codes must miss")
|
|
}
|
|
}
|