Files
paliad/internal/services/fristenrechner_proceedings_test.go
mAi 70985d88b0
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(fristenrechner): Slice S4 — Mode B wizard (m/paliad#146)
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.
2026-05-27 10:14:37 +02:00

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")
}
})
}