diff --git a/frontend/src/client/builder.ts b/frontend/src/client/builder.ts index ffb56df..2552af4 100644 --- a/frontend/src/client/builder.ts +++ b/frontend/src/client/builder.ts @@ -855,6 +855,16 @@ function openAddProceedingPicker(anchor: HTMLElement): void { if (!state.active) return; mountAddProceedingPicker(anchor, state.procTypes, async (meta) => { if (!state.active) return; + // Guard against a wire-shape regression: if the proceeding-types + // endpoint stops returning `id`, `meta.id` is undefined and + // JSON.stringify silently drops the field, the server rejects the + // POST with a 400, and fetchJSON swallows the error — the user + // sees "nothing happens" (t-paliad-345). Fail loud instead. + if (typeof meta.id !== "number" || meta.id <= 0) { + console.error("builder: missing proceeding_type id in picker meta", meta); + setSaveState("error"); + return; + } const proc = await addProceeding(state.active.id, { proceeding_type_id: meta.id, }); diff --git a/internal/services/fristenrechner.go b/internal/services/fristenrechner.go index 9f06d71..73c6115 100644 --- a/internal/services/fristenrechner.go +++ b/internal/services/fristenrechner.go @@ -145,7 +145,7 @@ func (s *FristenrechnerService) ListProceedings(ctx context.Context, opts Procee AND pe.event_kind = $%d )`, opts.EventKind) } - query := `SELECT code, name, name_en, jurisdiction + query := `SELECT id, code, name, name_en, jurisdiction FROM paliad.proceeding_types WHERE ` + strings.Join(where, " AND ") + ` ORDER BY sort_order` @@ -160,7 +160,7 @@ func (s *FristenrechnerService) ListProceedings(ctx context.Context, opts Procee for rows.Next() { var t lp.FristenrechnerType var juris sql.NullString - if err := rows.Scan(&t.Code, &t.Name, &t.NameEN, &juris); err != nil { + if err := rows.Scan(&t.ID, &t.Code, &t.Name, &t.NameEN, &juris); err != nil { return nil, err } if juris.Valid { diff --git a/pkg/litigationplanner/types.go b/pkg/litigationplanner/types.go index 19f393e..4a0468c 100644 --- a/pkg/litigationplanner/types.go +++ b/pkg/litigationplanner/types.go @@ -531,7 +531,17 @@ type RuleCalculationProceeding struct { // FristenrechnerType mirrors the /api/tools/proceeding-types response // metadata. +// +// ID is the paliad.proceeding_types primary key. Surfaces so frontend +// pickers (Litigation Builder add-proceeding, fristenrechner-wizard +// project prefill) can POST the FK directly without a code→id round +// trip. Historically code-keyed; the Litigation Builder POSTing +// proceeding_type_id (int) to /api/builder/scenarios/{id}/proceedings +// forced surfacing the id (t-paliad-345 — the missing id meant the +// POST silently sent body={} and the "+ Verfahren hinzufügen" button +// did nothing). type FristenrechnerType struct { + ID int `json:"id"` Code string `json:"code"` Name string `json:"name"` NameEN string `json:"nameEN"` diff --git a/pkg/litigationplanner/types_wire_test.go b/pkg/litigationplanner/types_wire_test.go new file mode 100644 index 0000000..84778ab --- /dev/null +++ b/pkg/litigationplanner/types_wire_test.go @@ -0,0 +1,50 @@ +package litigationplanner + +import ( + "encoding/json" + "strings" + "testing" +) + +// TestFristenrechnerType_WireShapeIncludesID is the regression test for +// t-paliad-345: the /api/tools/proceeding-types JSON response must +// include `id` so frontend pickers (Litigation Builder add-proceeding, +// fristenrechner-wizard project prefill) can POST proceeding_type_id +// directly without a code→id round trip. When the id was missing the +// Litigation Builder "+ Verfahren hinzufügen" button silently dropped +// the proceeding_type_id from the POST body (JSON.stringify omits +// undefined keys), the server rejected with 400, and the client +// swallowed the error — user-visible symptom was "nothing happens". +func TestFristenrechnerType_WireShapeIncludesID(t *testing.T) { + in := FristenrechnerType{ + ID: 42, + Code: "upc.inf.cfi", + Name: "UPC Verletzungsverfahren", + NameEN: "UPC Infringement Action", + Group: "UPC", + } + b, err := json.Marshal(in) + if err != nil { + t.Fatalf("marshal: %v", err) + } + got := string(b) + if !strings.Contains(got, `"id":42`) { + t.Errorf("missing id in wire shape: %s", got) + } + for _, want := range []string{`"code":"upc.inf.cfi"`, `"nameEN":"UPC Infringement Action"`} { + if !strings.Contains(got, want) { + t.Errorf("missing %q in wire shape: %s", want, got) + } + } + + // Round-trip — a client that posts the id back to /api/builder/ + // scenarios/{id}/proceedings should see it preserved as an integer + // (paliad.scenario_proceedings.proceeding_type_id is INT, not UUID). + var out FristenrechnerType + if err := json.Unmarshal(b, &out); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if out.ID != 42 { + t.Errorf("id lost on round-trip: got %d want 42", out.ID) + } +}