Mode B "🧭 Geführt" — the guided 3-5 row wizard defined in
docs/design-fristenrechner-overhaul-2026-05-26.md §3.2. Lands the
user on a single procedural_event (the trigger), then transitions
to the shared §4 result view.
Frontend:
* `fristenrechner-wizard.ts` — row stack with R1..R5:
R1 Was ist passiert? (event_kind, always asked)
R2 Vor welchem Gericht? (jurisdiction, skip if R1 narrows)
R3 In welchem Verfahren? (proceeding_type, auto-skip when
narrowed pool has 1 option)
R4 Welches Schriftstück? (procedural_event, landing)
R5 Welche Seite vertreten Sie? (party, only when follow-ups
differ by primary_party)
Row badges per §11.Q3: R1+R2 = Filter, R3+R4+R5 = Qualifier.
R5 has NO "Beide" option per §11.Q8 — Mode B is the file-mode
where perspective is a qualifier.
* Project prefill — derives R3 + R2 jurisdiction from
project.proceeding_type, R5 from project.our_side. Annotates
pre-filled rows with "aus Akte" tag and implicit rows with
"implizit" tag per §11.Q10 ("erhalten" annotation when a pick is
carried across an upstream change).
* R4-to-result transition — after R4 the wizard fetches /follow-
ups (no dates) to inspect primary_party variance. If both
claimant and defendant rules exist AND R5 isn't already set,
swaps the loading row for the R5 chip picker. Otherwise jumps
straight to mountResultView.
* URL state — `?mode=wizard&kind=…&forum=…&pt=…&r4=…&party=…`
keeps deep-link / back-nav consistent (the launchResult step
sets `event=` so the result view picks up).
* `fristenrechner-result.ts` mountModeShell now dispatches the
"wizard" tab to the wizard module (was a coming-soon
placeholder).
* 18 i18n keys added (DE + EN parity), 145-line CSS block for the
wizard row stack with Filter / Qualifier badge styling and
"aus Akte" annotation chip.
Backend:
* `ProceedingListOptions.EventKind` adds an EXISTS subquery
filter on `paliad.sequencing_rules` ⨯ `paliad.procedural_events`
so Mode B R3 chips only show proceedings whose event roster
contains at least one event of the requested kind (design
§6.3). Endpoint param: `event_kind=` on
/api/tools/proceeding-types.
Test updates:
* `TestListProceedings` switched from SKIP-when-column-missing to
asserting the live filter — mig 153 has landed, `kind` column
is in place. New subtests: kind=proceeding includes
upc.inf.cfi and excludes the phase row upc.cfi.interim;
event_kind=filing narrows to proceedings with filing events.
* `fristenrechner-wizard.test.ts` covers
`followUpsDifferByParty` — the R5 trigger predicate. 7 cases:
asymmetric → true; uniform / both / court / empty → false.
Verified — bun build clean (2971 i18n keys), 256 frontend tests
pass (incl. 7 new), go build + vet clean, live-DB
TestListProceedings passes all 6 subtests against mig 153 data.
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/lib/pq"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/db"
|
|
)
|
|
|
|
// TestListProceedings covers the proceeding chip-pool query that powers
|
|
// the Fristenrechner overhaul Mode A "Verfahren" filter strip (S3,
|
|
// design §3.1). The legacy callers go through ListFristenrechnerTypes
|
|
// (no filters) — that path stays green here. The new ListProceedings
|
|
// API accepts Jurisdiction + Kind filters; the Kind filter requires
|
|
// mig 153 to have landed, so this test skips the Kind=proceeding case
|
|
// when the column doesn't yet exist.
|
|
func TestListProceedings(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)
|
|
holidays := NewHolidayService(pool)
|
|
courts := NewCourtService(pool)
|
|
fr := NewFristenrechnerService(rules, holidays, courts)
|
|
|
|
t.Run("no filters returns the legacy fristenrechner set", func(t *testing.T) {
|
|
got, err := fr.ListProceedings(ctx, ProceedingListOptions{})
|
|
if err != nil {
|
|
t.Fatalf("list proceedings: %v", err)
|
|
}
|
|
if len(got) == 0 {
|
|
t.Fatalf("expected non-empty proceeding list")
|
|
}
|
|
// Sanity — upc.inf.cfi should always be in the active set.
|
|
found := false
|
|
for _, p := range got {
|
|
if p.Code == "upc.inf.cfi" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("upc.inf.cfi not in proceedings list")
|
|
}
|
|
})
|
|
|
|
t.Run("jurisdiction=UPC narrows to UPC-only", func(t *testing.T) {
|
|
got, err := fr.ListProceedings(ctx, ProceedingListOptions{Jurisdiction: "UPC"})
|
|
if err != nil {
|
|
t.Fatalf("list proceedings UPC: %v", err)
|
|
}
|
|
if len(got) == 0 {
|
|
t.Fatalf("expected UPC proceedings")
|
|
}
|
|
for _, p := range got {
|
|
if p.Group != "UPC" {
|
|
t.Errorf("non-UPC proceeding leaked: %s (group=%q)", p.Code, p.Group)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("jurisdiction=DE returns DE proceedings", func(t *testing.T) {
|
|
got, err := fr.ListProceedings(ctx, ProceedingListOptions{Jurisdiction: "DE"})
|
|
if err != nil {
|
|
t.Fatalf("list proceedings DE: %v", err)
|
|
}
|
|
if len(got) == 0 {
|
|
t.Fatalf("expected DE proceedings")
|
|
}
|
|
for _, p := range got {
|
|
if p.Group != "DE" {
|
|
t.Errorf("non-DE proceeding leaked: %s (group=%q)", p.Code, p.Group)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("ListFristenrechnerTypes legacy alias still works", func(t *testing.T) {
|
|
got, err := fr.ListFristenrechnerTypes(ctx)
|
|
if err != nil {
|
|
t.Fatalf("list fristenrechner types: %v", err)
|
|
}
|
|
if len(got) == 0 {
|
|
t.Fatalf("expected non-empty types")
|
|
}
|
|
})
|
|
|
|
t.Run("kind=proceeding narrows to primary proceedings only", func(t *testing.T) {
|
|
got, err := fr.ListProceedings(ctx, ProceedingListOptions{Kind: "proceeding"})
|
|
if err != nil {
|
|
t.Fatalf("list proceedings kind=proceeding: %v", err)
|
|
}
|
|
if len(got) == 0 {
|
|
t.Fatalf("expected non-empty primary-proceeding list")
|
|
}
|
|
// upc.inf.cfi is unambiguously a primary proceeding — must
|
|
// survive the filter.
|
|
found := false
|
|
for _, p := range got {
|
|
if p.Code == "upc.inf.cfi" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("upc.inf.cfi missing from kind=proceeding list")
|
|
}
|
|
// upc.cfi.interim is the canonical phase row (per mig 153 +
|
|
// taxonomy doc §0.4 Group B) — must NOT appear.
|
|
for _, p := range got {
|
|
if p.Code == "upc.cfi.interim" {
|
|
t.Errorf("phase row upc.cfi.interim leaked into kind=proceeding")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("event_kind=filing narrows to proceedings with filing events", func(t *testing.T) {
|
|
got, err := fr.ListProceedings(ctx, ProceedingListOptions{
|
|
Jurisdiction: "UPC",
|
|
Kind: "proceeding",
|
|
EventKind: "filing",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("list proceedings UPC+filing: %v", err)
|
|
}
|
|
if len(got) == 0 {
|
|
t.Fatalf("expected UPC proceedings with filing events")
|
|
}
|
|
// upc.inf.cfi has at least one rule anchored on a filing event
|
|
// (Klageerhebung, SoD, etc.) — must survive.
|
|
found := false
|
|
for _, p := range got {
|
|
if p.Code == "upc.inf.cfi" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("upc.inf.cfi missing from UPC + event_kind=filing list")
|
|
}
|
|
})
|
|
}
|