Files
paliad/internal/services/event_choice_service_test.go
mAi dc47ea7f43 feat(t-paliad-265): migration 129 + EventChoiceService (Slice A foundation)
m/paliad#96 — per-event-card optional choices on the Verfahrensablauf
timeline. This commit lands the schema + service layer.

Migration 129:
- paliad.project_event_choices table (project_id, submission_code,
  choice_kind ∈ {appellant, include_ccr, skip}, choice_value) with
  UNIQUE(project_id, submission_code, choice_kind) for idempotent
  re-pick, RLS via paliad.can_see_project.
- paliad.deadline_rules.choices_offered jsonb — opt-in declaration of
  which choice-kinds each rule offers. Seeded for every decision rule
  (appellant), every priority='optional' rule (skip), and the two
  Klageerwiderung rules (upc.inf.cfi.sod + de.inf.lg.erwidg) with
  include_ccr.

Live verification before authoring:
- rule_code is NULL on every decision row → submission_code is the
  join key (matches AnchorOverrides plumbing in fristenrechner.go).
- upc.inf.cfi.sod is the UPC Klageerwiderung, not upc.inf.cfi.def
  (rejected the design doc's first guess; SELECT name ILIKE
  'Klageerwiderung' confirmed).

Go service:
- models.ProjectEventChoice + DeadlineRule.ChoicesOffered.
- EventChoiceService: ListForProject / Upsert (with audit-log row to
  paliad.system_audit_log) / Delete. Pure-helper ToCalcOptionsAddendum
  + per-kind value validation + unit tests.

Design: docs/design-event-card-choices-2026-05-25.md §3 + §6.
2026-05-25 16:45:07 +02:00

109 lines
3.8 KiB
Go

package services
import (
"testing"
"mgit.msbls.de/m/paliad/internal/models"
)
// Unit tests for the pure helpers in event_choice_service.go. The CRUD
// path needs a live DB and lives in the integration suite.
func TestValidateChoice_Appellant(t *testing.T) {
for _, value := range []string{"claimant", "defendant", "both", "none"} {
if err := validateChoice("appellant", value); err != nil {
t.Errorf("appellant=%q should pass, got %v", value, err)
}
}
for _, bad := range []string{"", "applicant", "true", "claimaant"} {
if err := validateChoice("appellant", bad); err == nil {
t.Errorf("appellant=%q should fail validation", bad)
}
}
}
func TestValidateChoice_IncludeCCR(t *testing.T) {
for _, value := range []string{"true", "false"} {
if err := validateChoice("include_ccr", value); err != nil {
t.Errorf("include_ccr=%q should pass, got %v", value, err)
}
}
for _, bad := range []string{"", "yes", "1", "True"} {
if err := validateChoice("include_ccr", bad); err == nil {
t.Errorf("include_ccr=%q should fail validation", bad)
}
}
}
func TestValidateChoice_Skip(t *testing.T) {
for _, value := range []string{"true", "false"} {
if err := validateChoice("skip", value); err != nil {
t.Errorf("skip=%q should pass, got %v", value, err)
}
}
if err := validateChoice("skip", "maybe"); err == nil {
t.Errorf("skip=maybe should fail")
}
}
func TestValidateChoice_UnknownKind(t *testing.T) {
if err := validateChoice("not_a_kind", "true"); err == nil {
t.Errorf("unknown choice_kind should fail")
}
}
func TestToCalcOptionsAddendum_PerCardAppellant(t *testing.T) {
choices := []models.ProjectEventChoice{
{SubmissionCode: "upc.inf.cfi.decision", ChoiceKind: "appellant", ChoiceValue: "defendant"},
{SubmissionCode: "de.inf.lg.urteil", ChoiceKind: "appellant", ChoiceValue: "both"},
}
out := ToCalcOptionsAddendum(choices)
if out.PerCardAppellant["upc.inf.cfi.decision"] != "defendant" {
t.Errorf("appellant pick for upc.inf.cfi.decision = %q, want defendant", out.PerCardAppellant["upc.inf.cfi.decision"])
}
if out.PerCardAppellant["de.inf.lg.urteil"] != "both" {
t.Errorf("appellant pick for de.inf.lg.urteil = %q, want both", out.PerCardAppellant["de.inf.lg.urteil"])
}
if len(out.SkipRules) != 0 || len(out.IncludeCCRFor) != 0 {
t.Errorf("appellant-only input should not populate skip/include_ccr maps")
}
}
func TestToCalcOptionsAddendum_SkipRules(t *testing.T) {
choices := []models.ProjectEventChoice{
{SubmissionCode: "upc.inf.cfi.ccr", ChoiceKind: "skip", ChoiceValue: "true"},
{SubmissionCode: "upc.inf.cfi.prelim", ChoiceKind: "skip", ChoiceValue: "false"},
}
out := ToCalcOptionsAddendum(choices)
if _, ok := out.SkipRules["upc.inf.cfi.ccr"]; !ok {
t.Errorf("skip=true should populate SkipRules")
}
if _, ok := out.SkipRules["upc.inf.cfi.prelim"]; ok {
t.Errorf("skip=false should NOT populate SkipRules")
}
}
func TestToCalcOptionsAddendum_IncludeCCRFor(t *testing.T) {
choices := []models.ProjectEventChoice{
{SubmissionCode: "upc.inf.cfi.sod", ChoiceKind: "include_ccr", ChoiceValue: "true"},
{SubmissionCode: "de.inf.lg.erwidg", ChoiceKind: "include_ccr", ChoiceValue: "false"},
}
out := ToCalcOptionsAddendum(choices)
if _, ok := out.IncludeCCRFor["upc.inf.cfi.sod"]; !ok {
t.Errorf("include_ccr=true should populate IncludeCCRFor")
}
if _, ok := out.IncludeCCRFor["de.inf.lg.erwidg"]; ok {
t.Errorf("include_ccr=false should NOT populate IncludeCCRFor")
}
}
func TestToCalcOptionsAddendum_EmptyInput(t *testing.T) {
out := ToCalcOptionsAddendum(nil)
if out.PerCardAppellant == nil || out.SkipRules == nil || out.IncludeCCRFor == nil {
t.Errorf("empty input should still produce non-nil maps for safe indexing")
}
if len(out.PerCardAppellant) != 0 || len(out.SkipRules) != 0 || len(out.IncludeCCRFor) != 0 {
t.Errorf("empty input should produce empty maps")
}
}