Workstream B Go sweep — matches mig 098. Every place the deadline-rules service reads/writes the per-rule identifier now uses the new column name and the new struct field. Distinct from rule_code (legal citation) and from proceeding_types.code (the proceeding's 3-segment code). Touch points: - models.DeadlineRule.Code → SubmissionCode (db + json tags renamed in lockstep — JSON contract `submission_code` is the new shape). - deadline_rule_service: ruleColumns SELECT list updated. - rule_editor_service: CreateRuleInput.Code → SubmissionCode (json tag too), INSERT + CloneAsDraft SELECT updated. - projection_service: lookupRuleByCode → lookupRuleBySubmissionCode (SQL WHERE clause + error message); every r.Code / parent.Code / rule.Code / first.Code / src.rule.Code read renamed. - fristenrechner: r.Code / prev.Code / rule.Code reads renamed in Calculate (parent-anchor + override-key + computed-by-code map) and in CalculateRule's LocalCode emission; the proceeding-code+submission- code resolver query uses `submission_code = $2`. - event_trigger_service / deadline_calculator: r.Code reads renamed. UIDeadline.Code (the calculator's wire response) is unchanged — that field is a separate API contract pointing at the same value; renaming it would force every frontend deadline-renderer through a contract break that isn't part of this workstream. Test fixtures updated to the new SubmissionCode field name; live-DB tests updated to the post-mig-098 prefixed values (`inf.sod` → `upc.inf.cfi.sod` etc.). New submission_codes_shape_test asserts every active+published row matches the 4+-segment proceeding-prefixed shape (sibling of TestProceedingCodeShape; mirrors mig 098 §6.1). go build ./... clean. go test ./internal/... green.
172 lines
6.0 KiB
Go
172 lines
6.0 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/models"
|
|
)
|
|
|
|
func ptr[T any](v T) *T { return &v }
|
|
|
|
func TestCalculateEndDate_Days(t *testing.T) {
|
|
holidays := NewHolidayService(nil)
|
|
calc := NewDeadlineCalculator(holidays)
|
|
|
|
rule := models.DeadlineRule{
|
|
ID: uuid.New(),
|
|
Name: "Test rule",
|
|
DurationValue: 10,
|
|
DurationUnit: "days",
|
|
Timing: ptr("after"),
|
|
}
|
|
// 2026-01-13 (Tue) + 10 days = 2026-01-23 (Fri) — no adjustment.
|
|
in := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
|
|
adjusted, original, wasAdjusted := calc.CalculateEndDate(in, rule, "DE", "UPC")
|
|
want := time.Date(2026, 1, 23, 0, 0, 0, 0, time.UTC)
|
|
|
|
if !adjusted.Equal(want) {
|
|
t.Errorf("adjusted: got %s, want %s", adjusted, want)
|
|
}
|
|
if !original.Equal(want) {
|
|
t.Errorf("original: got %s, want %s", original, want)
|
|
}
|
|
if wasAdjusted {
|
|
t.Error("did not expect adjustment for working day")
|
|
}
|
|
}
|
|
|
|
func TestCalculateEndDate_Months_HolidayAdjust(t *testing.T) {
|
|
holidays := NewHolidayService(nil)
|
|
calc := NewDeadlineCalculator(holidays)
|
|
|
|
rule := models.DeadlineRule{
|
|
ID: uuid.New(),
|
|
Name: "3-month rule",
|
|
DurationValue: 3,
|
|
DurationUnit: "months",
|
|
Timing: ptr("after"),
|
|
}
|
|
// 2026-01-01 (Neujahr) + 3 months = 2026-04-01.
|
|
// 2026-04-01 = Wednesday → working day, no adjust.
|
|
in := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
adjusted, original, _ := calc.CalculateEndDate(in, rule, "DE", "UPC")
|
|
wantOrig := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
|
if !original.Equal(wantOrig) {
|
|
t.Errorf("original: got %s, want %s", original, wantOrig)
|
|
}
|
|
if !adjusted.Equal(wantOrig) {
|
|
t.Errorf("adjusted: got %s, want %s", adjusted, wantOrig)
|
|
}
|
|
}
|
|
|
|
func TestCalculateEndDate_Weeks_LandsOnHoliday(t *testing.T) {
|
|
holidays := NewHolidayService(nil)
|
|
calc := NewDeadlineCalculator(holidays)
|
|
|
|
rule := models.DeadlineRule{
|
|
ID: uuid.New(),
|
|
Name: "2-week rule",
|
|
DurationValue: 2,
|
|
DurationUnit: "weeks",
|
|
Timing: ptr("after"),
|
|
}
|
|
// Land on Karfreitag 2026 (2026-04-03, Friday).
|
|
// Trigger: 2026-03-20 (Friday). +2 weeks = 2026-04-03 = Karfreitag.
|
|
// Adjust: skip Karfreitag (Fri), Sat, Sun, Ostermontag (Mon 04-06), to
|
|
// Tuesday 2026-04-07.
|
|
in := time.Date(2026, 3, 20, 0, 0, 0, 0, time.UTC)
|
|
adjusted, original, wasAdjusted := calc.CalculateEndDate(in, rule, "DE", "UPC")
|
|
wantOrig := time.Date(2026, 4, 3, 0, 0, 0, 0, time.UTC)
|
|
wantAdj := time.Date(2026, 4, 7, 0, 0, 0, 0, time.UTC)
|
|
|
|
if !original.Equal(wantOrig) {
|
|
t.Errorf("original: got %s, want %s", original, wantOrig)
|
|
}
|
|
if !adjusted.Equal(wantAdj) {
|
|
t.Errorf("adjusted: got %s, want %s", adjusted, wantAdj)
|
|
}
|
|
if !wasAdjusted {
|
|
t.Error("expected wasAdjusted=true")
|
|
}
|
|
}
|
|
|
|
func TestCalculateEndDate_BeforeTiming(t *testing.T) {
|
|
holidays := NewHolidayService(nil)
|
|
calc := NewDeadlineCalculator(holidays)
|
|
|
|
rule := models.DeadlineRule{
|
|
ID: uuid.New(),
|
|
Name: "1-month before",
|
|
DurationValue: 1,
|
|
DurationUnit: "months",
|
|
Timing: ptr("before"),
|
|
}
|
|
// "before" subtracts: 2026-04-15 - 1 month = 2026-03-15 (Sunday).
|
|
// Adjust: Sunday → Monday 2026-03-16.
|
|
in := time.Date(2026, 4, 15, 0, 0, 0, 0, time.UTC)
|
|
adjusted, _, _ := calc.CalculateEndDate(in, rule, "DE", "UPC")
|
|
want := time.Date(2026, 3, 16, 0, 0, 0, 0, time.UTC)
|
|
if !adjusted.Equal(want) {
|
|
t.Errorf("adjusted: got %s, want %s", adjusted, want)
|
|
}
|
|
}
|
|
|
|
func TestCalculateFromRules_BatchAndZeroDuration(t *testing.T) {
|
|
holidays := NewHolidayService(nil)
|
|
calc := NewDeadlineCalculator(holidays)
|
|
|
|
rules := []models.DeadlineRule{
|
|
{ID: uuid.New(), Name: "Filing", DurationValue: 0, DurationUnit: "months"},
|
|
{ID: uuid.New(), Name: "Defence", SubmissionCode: ptr("upc.inf.cfi.sod"), DurationValue: 3, DurationUnit: "months", Timing: ptr("after")},
|
|
}
|
|
in := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
|
|
results := calc.CalculateFromRules(in, rules, "DE", "UPC")
|
|
|
|
if len(results) != 2 {
|
|
t.Fatalf("got %d results, want 2", len(results))
|
|
}
|
|
// Zero-duration rule returns the trigger date unchanged.
|
|
if results[0].DueDate != "2026-01-13" {
|
|
t.Errorf("zero-duration rule: got %s, want 2026-01-13", results[0].DueDate)
|
|
}
|
|
// 3-month rule → 2026-04-13 (Monday, working).
|
|
if results[1].DueDate != "2026-04-13" {
|
|
t.Errorf("3-month rule: got %s, want 2026-04-13", results[1].DueDate)
|
|
}
|
|
if results[1].RuleCode != "upc.inf.cfi.sod" {
|
|
t.Errorf("rule code: got %q, want upc.inf.cfi.sod", results[1].RuleCode)
|
|
}
|
|
}
|
|
|
|
// PR-3 audit fix: AdjustForNonWorkingDays must walk past the full UPC summer
|
|
// vacation (~33 weekdays) plus the flanking weekend without bailing on the
|
|
// 30-iteration cap. Pre-PR-3 a SoD on 2026-04-30 (3mo from trigger) would
|
|
// adjust to Sat 2026-08-29 (mid-walk, off-by-31). Correct landing is Mon
|
|
// 2026-08-31 (UPC vacation ends Aug 28 Fri; weekend skipped).
|
|
func TestAdjustForNonWorkingDays_WalksPastSummerVacation(t *testing.T) {
|
|
holidays := NewHolidayService(nil) // pure German federal holidays — no UPC vacation
|
|
// To reproduce the production case (which has UPC summer vacation seeded
|
|
// in paliad.holidays), we shim the holiday service with a custom config
|
|
// matching migration 010's UPC summer vacation.
|
|
// Without the seed, a Thu 2026-07-30 + 0 days adjustment is a no-op
|
|
// (working day), which doesn't exercise the cap. Skip if running without
|
|
// the production seed.
|
|
in := time.Date(2026, 7, 30, 0, 0, 0, 0, time.UTC)
|
|
if holidays.IsNonWorkingDay(in, "DE", "UPC") {
|
|
t.Skip("Thu 2026-07-30 unexpectedly flagged as non-working without UPC seed")
|
|
}
|
|
// Sanity: with no UPC vacation, Thu 2026-07-30 + 0 → unchanged.
|
|
adjusted, _, wasAdjusted := holidays.AdjustForNonWorkingDays(in, "DE", "UPC")
|
|
if wasAdjusted {
|
|
t.Errorf("expected no adjustment without UPC seed; got %s", adjusted)
|
|
}
|
|
// The behaviour we actually rely on (cap=60) is only observable against
|
|
// the seeded paliad.holidays. The production smoke test for t-paliad-086
|
|
// PR-3 ("SoD 3mo from 2026-04-30 → adjusted Mon 2026-08-31, not Sat
|
|
// 2026-08-29") locks the live behaviour.
|
|
}
|
|
|