Tightens paliad.deadline_rules.primary_party from free-text to a CHECK
constraint over the canonical four-value vocab (claimant / defendant /
court / both). NULL stays valid for the 78 cross-cutting orphan
concept seeds (Wiedereinsetzung, Versäumnisurteil-Einspruch,
Schriftsatznachreichung, Weiterbehandlung) — they have no
proceeding_type_id binding so they're outside the calculator's path;
loosening the CHECK to "IS NULL OR IN (…)" keeps them valid without
backfill gymnastics.
Migration 135 (audit-first):
- DO block RAISEs NOTICE for every non-conforming row + RAISEs
EXCEPTION if any dirty rows exist (manual cleanup required).
Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows
on the current corpus; the audit pass stays in the migration as
safety against future drift.
- ALTER TABLE … ADD CONSTRAINT deadline_rules_primary_party_chk
CHECK (primary_party IS NULL OR primary_party IN
('claimant', 'defendant', 'court', 'both'))
- Post-migration distribution NOTICE so the operator sees the
final per-value count.
- Down = DROP CONSTRAINT. No data revert needed.
Package additions (pkg/litigationplanner):
- PrimaryParty* constants (PrimaryPartyClaimant / Defendant / Court
/ Both) + PrimaryParties[] ordered list + IsValidPrimaryParty(s)
predicate. Empty string is "no value supplied" = valid (NULL maps
to empty on the wire); non-empty must match one of the four
canonical values.
- Sibling unit tests (primary_party_test.go) pin the four-value
vocab + the chip order + IsValidAppealTarget's matching shape.
Rule-editor validation hook (rule_editor_service.go):
- Create() validates input.PrimaryParty before INSERT.
- UpdateDraft() validates patch.PrimaryParty before UPDATE.
- Both surface a user-friendly 400 with the canonical vocab listed
instead of leaking the raw PG CHECK constraint-violation message.
- Uses errors.Is(err, ErrInvalidInput) so handler 400 routing
continues to work.
services/fristenrechner.go cleanup:
- The B2-inlined isValidPartyForLookup helper is replaced with the
canonical lp.IsValidPrimaryParty. No behaviour change.
No frontend changes — the rule-editor's primary_party UI already
constrains to the four values via a select; the validation hook is
defense-in-depth.
Audit:
- go build + go test (incl. new lp unit tests) all green
- Pre-migration audit confirmed: 26 claimant + 26 defendant + 38
court + 63 both + 78 NULL = 231 total, all in canonical vocab
- event_categories.party (text[] array, narrower semantic) is
NOT touched in this migration per the design doc's
"out of scope, separate follow-up" decision
69 lines
2.0 KiB
Go
69 lines
2.0 KiB
Go
package litigationplanner
|
|
|
|
import "testing"
|
|
|
|
// TestIsValidPrimaryParty pins the four-value vocab + NULL-equivalent
|
|
// behaviour the rule-editor's B3 validation hook depends on. Empty
|
|
// string is "no value supplied" = valid (NULL maps to empty on the
|
|
// wire). Non-empty must match one of the four canonical values.
|
|
func TestIsValidPrimaryParty(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", true},
|
|
{"claimant", true},
|
|
{"defendant", true},
|
|
{"court", true},
|
|
{"both", true},
|
|
{"Claimant", false}, // case-sensitive
|
|
{"clamant", false}, // typo
|
|
{"applicant", false}, // not in vocab
|
|
{"foo", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsValidPrimaryParty(c.in); got != c.want {
|
|
t.Errorf("IsValidPrimaryParty(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPrimaryPartiesOrder pins the canonical chip order (admin UI
|
|
// renders these as a select; reordering would break user muscle
|
|
// memory). Update both the slice + this test together if the order
|
|
// genuinely needs to change.
|
|
func TestPrimaryPartiesOrder(t *testing.T) {
|
|
want := []string{"claimant", "defendant", "court", "both"}
|
|
if len(PrimaryParties) != len(want) {
|
|
t.Fatalf("PrimaryParties has %d entries, want %d", len(PrimaryParties), len(want))
|
|
}
|
|
for i, p := range PrimaryParties {
|
|
if p != want[i] {
|
|
t.Errorf("PrimaryParties[%d] = %q, want %q", i, p, want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIsValidAppealTarget is sibling-of: same shape, ensures the B1
|
|
// helper has the same NULL-equivalent semantic.
|
|
func TestIsValidAppealTarget(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", true},
|
|
{"endentscheidung", true},
|
|
{"kostenentscheidung", true},
|
|
{"anordnung", true},
|
|
{"schadensbemessung", true},
|
|
{"bucheinsicht", true},
|
|
{"foo", false},
|
|
{"Endentscheidung", false}, // case-sensitive
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsValidAppealTarget(c.in); got != c.want {
|
|
t.Errorf("IsValidAppealTarget(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|