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.
109 lines
3.8 KiB
Go
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")
|
|
}
|
|
}
|