Optional events anchored on the same trigger (e.g. the four post-Entscheidung rules in upc.inf.cfi) used to render in catalog sequence_order, so a 2-month rule (R.118.4 Folgeentscheidungen) would precede a 1-month rule (R.151 Kostenentscheidung) chained off the same decision. Now the calculator does a post-evaluation permutation pass that sorts consecutive same-parent rows by duration ascending — days < weeks < months < years, ties broken by duration_value then submission_code. Different trigger groups keep their proceeding-sequence position — the walk only ever permutes rows that already share a parent. Root rules (no parent) are never sorted against each other. Court-set / conditional rows whose date isn't in the duration ladder sort LAST within their group. Verified order against m's report: R.151 cost_app + R.353 rectification (1-month tier) now render before R.220.1 appeal_spawn + R.118.4 cons_orders (2-month tier). Issue: m/paliad#128
222 lines
7.9 KiB
Go
222 lines
7.9 KiB
Go
package services
|
|
|
|
// Pure-function tests for the trigger-group duration sort introduced
|
|
// by t-paliad-296 / m/paliad#128. No DB needed — feeds synthetic
|
|
// UIDeadlines and a ruleByID map directly into the helper.
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/models"
|
|
)
|
|
|
|
// makeRule is a tiny constructor for a synthetic rule with just the
|
|
// fields the sort reads (parent_id, duration_value, duration_unit,
|
|
// submission_code, trigger_event_id).
|
|
func makeRule(t *testing.T, parent *uuid.UUID, code string, val int, unit string) (uuid.UUID, models.DeadlineRule) {
|
|
t.Helper()
|
|
id := uuid.New()
|
|
codeCopy := code
|
|
return id, models.DeadlineRule{
|
|
ID: id,
|
|
ParentID: parent,
|
|
SubmissionCode: &codeCopy,
|
|
DurationValue: val,
|
|
DurationUnit: unit,
|
|
}
|
|
}
|
|
|
|
func makeDeadline(id uuid.UUID, code string) UIDeadline {
|
|
return UIDeadline{
|
|
RuleID: id.String(),
|
|
Code: code,
|
|
}
|
|
}
|
|
|
|
// TestSortDeadlinesByDurationWithinTriggerGroup_PostDecision is the
|
|
// canonical scenario from m's report — four post-decision optional
|
|
// events anchored on the same decision must render with 1-month rules
|
|
// before 2-month rules.
|
|
func TestSortDeadlinesByDurationWithinTriggerGroup_PostDecision(t *testing.T) {
|
|
decisionID := uuid.New()
|
|
|
|
// Catalog order matches mig 132 sequence_order: cons_orders(60),
|
|
// cost_app(70), rectification(70), appeal_spawn(80).
|
|
consOrdID, consOrdRule := makeRule(t, &decisionID, "upc.inf.cfi.cons_orders", 2, "months")
|
|
costAppID, costAppRule := makeRule(t, &decisionID, "upc.inf.cfi.cost_app", 1, "months")
|
|
rectID, rectRule := makeRule(t, &decisionID, "upc.inf.cfi.rectification", 1, "months")
|
|
appealID, appealRule := makeRule(t, &decisionID, "upc.inf.cfi.appeal_spawn", 2, "months")
|
|
|
|
ruleByID := map[uuid.UUID]models.DeadlineRule{
|
|
consOrdID: consOrdRule,
|
|
costAppID: costAppRule,
|
|
rectID: rectRule,
|
|
appealID: appealRule,
|
|
}
|
|
|
|
deadlines := []UIDeadline{
|
|
makeDeadline(consOrdID, "upc.inf.cfi.cons_orders"),
|
|
makeDeadline(costAppID, "upc.inf.cfi.cost_app"),
|
|
makeDeadline(rectID, "upc.inf.cfi.rectification"),
|
|
makeDeadline(appealID, "upc.inf.cfi.appeal_spawn"),
|
|
}
|
|
|
|
sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
|
|
|
|
// 1-month tier first (cost_app, rectification — alphabetical by
|
|
// submission_code), then 2-month tier (appeal_spawn, cons_orders
|
|
// — submission_code ASC tiebreak per spec).
|
|
want := []string{
|
|
"upc.inf.cfi.cost_app",
|
|
"upc.inf.cfi.rectification",
|
|
"upc.inf.cfi.appeal_spawn",
|
|
"upc.inf.cfi.cons_orders",
|
|
}
|
|
for i, w := range want {
|
|
if deadlines[i].Code != w {
|
|
t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSortDeadlinesByDurationWithinTriggerGroup_UnitWeight asserts the
|
|
// unit-weight ordering: days < weeks < months < years, with shorter
|
|
// durations of the same unit winning their tier.
|
|
func TestSortDeadlinesByDurationWithinTriggerGroup_UnitWeight(t *testing.T) {
|
|
parentID := uuid.New()
|
|
|
|
d14ID, d14Rule := makeRule(t, &parentID, "x.14days", 14, "days")
|
|
d2wID, d2wRule := makeRule(t, &parentID, "x.2weeks", 2, "weeks")
|
|
d1mID, d1mRule := makeRule(t, &parentID, "x.1month", 1, "months")
|
|
d6mID, d6mRule := makeRule(t, &parentID, "x.6months", 6, "months")
|
|
d1yID, d1yRule := makeRule(t, &parentID, "x.1year", 1, "years")
|
|
|
|
ruleByID := map[uuid.UUID]models.DeadlineRule{
|
|
d14ID: d14Rule, d2wID: d2wRule, d1mID: d1mRule, d6mID: d6mRule, d1yID: d1yRule,
|
|
}
|
|
|
|
deadlines := []UIDeadline{
|
|
makeDeadline(d6mID, "x.6months"),
|
|
makeDeadline(d1yID, "x.1year"),
|
|
makeDeadline(d2wID, "x.2weeks"),
|
|
makeDeadline(d14ID, "x.14days"),
|
|
makeDeadline(d1mID, "x.1month"),
|
|
}
|
|
|
|
sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
|
|
|
|
want := []string{"x.14days", "x.2weeks", "x.1month", "x.6months", "x.1year"}
|
|
for i, w := range want {
|
|
if deadlines[i].Code != w {
|
|
t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSortDeadlinesByDurationWithinTriggerGroup_NoCrossGroupReorder
|
|
// guards the hard rule: rules with different parents must keep their
|
|
// relative position. Sorting only ever permutes adjacent same-parent
|
|
// rows.
|
|
func TestSortDeadlinesByDurationWithinTriggerGroup_NoCrossGroupReorder(t *testing.T) {
|
|
parentAID := uuid.New()
|
|
parentBID := uuid.New()
|
|
|
|
a3mID, a3mRule := makeRule(t, &parentAID, "ga.3months", 3, "months")
|
|
b1mID, b1mRule := makeRule(t, &parentBID, "gb.1month", 1, "months")
|
|
a14dID, a14dRule := makeRule(t, &parentAID, "ga.14days", 14, "days")
|
|
b2mID, b2mRule := makeRule(t, &parentBID, "gb.2months", 2, "months")
|
|
|
|
ruleByID := map[uuid.UUID]models.DeadlineRule{
|
|
a3mID: a3mRule, b1mID: b1mRule, a14dID: a14dRule, b2mID: b2mRule,
|
|
}
|
|
|
|
// Interleaved groups: A, B, A, B. Each group has one rule between
|
|
// each other group's rules — the consecutive-run walk should treat
|
|
// each as its own one-element run and not reorder anything.
|
|
deadlines := []UIDeadline{
|
|
makeDeadline(a3mID, "ga.3months"),
|
|
makeDeadline(b1mID, "gb.1month"),
|
|
makeDeadline(a14dID, "ga.14days"),
|
|
makeDeadline(b2mID, "gb.2months"),
|
|
}
|
|
|
|
sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
|
|
|
|
want := []string{"ga.3months", "gb.1month", "ga.14days", "gb.2months"}
|
|
for i, w := range want {
|
|
if deadlines[i].Code != w {
|
|
t.Errorf("deadlines[%d].Code = %q, want %q (interleaved groups must not reorder across)", i, deadlines[i].Code, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSortDeadlinesByDurationWithinTriggerGroup_ConditionalLast asserts
|
|
// that court-set / conditional rows (no concrete date in the duration
|
|
// ladder) sort LAST within their group, regardless of their stated
|
|
// duration value.
|
|
func TestSortDeadlinesByDurationWithinTriggerGroup_ConditionalLast(t *testing.T) {
|
|
parentID := uuid.New()
|
|
|
|
dID, dRule := makeRule(t, &parentID, "x.duration", 2, "months")
|
|
cID, cRule := makeRule(t, &parentID, "x.conditional", 1, "months")
|
|
csID, csRule := makeRule(t, &parentID, "x.courtset", 1, "months")
|
|
d2ID, d2Rule := makeRule(t, &parentID, "x.short", 14, "days")
|
|
|
|
ruleByID := map[uuid.UUID]models.DeadlineRule{
|
|
dID: dRule, cID: cRule, csID: csRule, d2ID: d2Rule,
|
|
}
|
|
|
|
deadlines := []UIDeadline{
|
|
{RuleID: cID.String(), Code: "x.conditional", IsConditional: true},
|
|
{RuleID: dID.String(), Code: "x.duration"},
|
|
{RuleID: csID.String(), Code: "x.courtset", IsCourtSet: true},
|
|
{RuleID: d2ID.String(), Code: "x.short"},
|
|
}
|
|
|
|
sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
|
|
|
|
// Concrete rows first (sorted by duration): x.short (14d) then
|
|
// x.duration (2mo). Then the two no-date rows, tiebroken by code:
|
|
// x.conditional < x.courtset alphabetically.
|
|
want := []string{"x.short", "x.duration", "x.conditional", "x.courtset"}
|
|
for i, w := range want {
|
|
if deadlines[i].Code != w {
|
|
t.Errorf("deadlines[%d].Code = %q, want %q", i, deadlines[i].Code, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSortDeadlinesByDurationWithinTriggerGroup_RootsNotMerged guards
|
|
// the root-rule exception: top-level rules (parent_id=nil, no
|
|
// trigger_event_id) must never be sorted against each other — they
|
|
// represent distinct anchor points (SoC vs oral hearing vs decision)
|
|
// whose proceeding-sequence order is non-negotiable.
|
|
func TestSortDeadlinesByDurationWithinTriggerGroup_RootsNotMerged(t *testing.T) {
|
|
rootSoCID, rootSoCRule := makeRule(t, nil, "x.soc", 0, "months")
|
|
rootOralID, rootOralRule := makeRule(t, nil, "x.oral", 0, "months")
|
|
rootDecID, rootDecRule := makeRule(t, nil, "x.decision", 0, "months")
|
|
|
|
ruleByID := map[uuid.UUID]models.DeadlineRule{
|
|
rootSoCID: rootSoCRule, rootOralID: rootOralRule, rootDecID: rootDecRule,
|
|
}
|
|
|
|
deadlines := []UIDeadline{
|
|
makeDeadline(rootSoCID, "x.soc"),
|
|
makeDeadline(rootOralID, "x.oral"),
|
|
makeDeadline(rootDecID, "x.decision"),
|
|
}
|
|
|
|
sortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
|
|
|
|
// Roots must keep their input order — they're not in the same
|
|
// trigger group as each other.
|
|
want := []string{"x.soc", "x.oral", "x.decision"}
|
|
for i, w := range want {
|
|
if deadlines[i].Code != w {
|
|
t.Errorf("deadlines[%d].Code = %q, want %q (roots must not be sorted against each other)", i, deadlines[i].Code, w)
|
|
}
|
|
}
|
|
}
|