Holiday struct gains Country (ISO-3166) + Regime ('UPC' | 'EPO' | "")
fields. AppliesTo(country, regime) is the matching rule the new lookup
methods filter through: a row matches when its Country equals the
court's country OR its Regime equals the court's regime. UPC LD München
(DE+UPC) sees DE federal + UPC vacations; LG München (DE+"") sees only
DE federal; UPC LD Paris (FR+UPC) sees FR + UPC. germanFederalHolidays
fallback now country-tagged 'DE' so the per-country filter applies it
only to DE-jurisdictional callers.
Public service methods (IsHoliday, IsNonWorkingDay, AdjustForNonWorking
Days, AdjustForNonWorkingDaysWithReason, findVacationBlock) all take
(country, regime). Cache stays year-keyed — single DB hit per year, all
courts touching that year share it.
New CourtService loads paliad.courts once + answers Lookup(id),
CountryRegime(id, defaultCountry, defaultRegime), All(), ByCourtType(t).
FristenrechnerService.CalcOptions / CalcRuleParams gain CourtID;
EventDeadlineService.Calculate gains courtID. When courtID is empty,
DefaultsForJurisdiction maps the proceeding's existing jurisdiction
column to a sensible (country, regime) default — UPC proceedings get
(DE, UPC), everything else gets DE-only — preserving today's behaviour
for callers that don't yet send a court.
Tests: new TestAppliesTo_CountryRegimeFilter + TestAppliesTo_Rules
cover the cross-product of (DE court / UPC LD München / UPC LD Paris /
LG München) × (DE federal / UPC vacation / FR holiday). Existing tests
threaded through with ('DE', 'UPC') to preserve behaviour they were
written to lock.
129 lines
5.0 KiB
Go
129 lines
5.0 KiB
Go
package services
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// addWorkingDays + composite-rule semantics — pure-Go logic, no DB needed.
|
|
|
|
func TestAddWorkingDays_SkipsWeekends(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
|
|
// 2026-04-30 = Thu. +3 wd: step → Fri May 1 (Tag der Arbeit, skip) → Sat
|
|
// (skip) → Sun (skip) → Mon May 4 = WD 1; → Tue May 5 = WD 2; → Wed
|
|
// May 6 = WD 3. So +3 wd = Wed 2026-05-06.
|
|
in := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC)
|
|
got := s.addWorkingDays(in, 3, "DE", "UPC")
|
|
want := time.Date(2026, 5, 6, 0, 0, 0, 0, time.UTC)
|
|
if !got.Equal(want) {
|
|
t.Errorf("addWorkingDays(+3): got %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAddWorkingDays_SkipsHolidays(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
|
|
// 2026-04-30 = Thu. +1 wd = Fri 2026-05-01 = Tag der Arbeit (DE federal holiday).
|
|
// → skip → Sat (weekend) → skip → Sun (weekend) → skip → Mon 2026-05-04.
|
|
in := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC)
|
|
got := s.addWorkingDays(in, 1, "DE", "UPC")
|
|
want := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC)
|
|
if !got.Equal(want) {
|
|
t.Errorf("addWorkingDays(+1) over Tag der Arbeit: got %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAddWorkingDays_NegativeWalksBackward(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
|
|
// Mon 2026-05-04 - 2 wd = Thu 2026-04-30 (skipping Fri 2026-05-01 holiday).
|
|
// Walk: -1 wd → Fri 05-01 → holiday → Thu 04-30 = working. 1 wd done.
|
|
// -1 wd → Wed 04-29. 2 wd done.
|
|
in := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC)
|
|
got := s.addWorkingDays(in, -2, "DE", "UPC")
|
|
want := time.Date(2026, 4, 29, 0, 0, 0, 0, time.UTC)
|
|
if !got.Equal(want) {
|
|
t.Errorf("addWorkingDays(-2) over Tag der Arbeit: got %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAddWorkingDays_Zero(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
|
|
// Day-zero convention: returns input unchanged, even if it's a weekend.
|
|
weekend := time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC) // Saturday
|
|
got := s.addWorkingDays(weekend, 0, "DE", "UPC")
|
|
if !got.Equal(weekend) {
|
|
t.Errorf("addWorkingDays(0) on weekend: got %s, want %s (unchanged)", got, weekend)
|
|
}
|
|
}
|
|
|
|
func TestApplyDuration_WorkingDays_SkipsAdjustment(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
|
|
// working_days lands on a working day by construction → no further adjust.
|
|
// Thu 2026-04-30 + 1 wd = Mon 2026-05-04 (skipped Fri holiday + weekend).
|
|
in := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC)
|
|
raw, adjusted, didAdjust := s.applyDuration(in, 1, "working_days", "after", "DE", "UPC")
|
|
want := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC)
|
|
|
|
if !raw.Equal(want) {
|
|
t.Errorf("raw: got %s, want %s", raw, want)
|
|
}
|
|
if !adjusted.Equal(want) {
|
|
t.Errorf("adjusted: got %s, want %s", adjusted, want)
|
|
}
|
|
if didAdjust {
|
|
t.Error("working_days unit should report didAdjust=false")
|
|
}
|
|
}
|
|
|
|
func TestApplyDuration_BeforeTiming(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
|
|
// Wed 2026-04-15 - 2 weeks = Wed 2026-04-01. Working day → no adjust.
|
|
in := time.Date(2026, 4, 15, 0, 0, 0, 0, time.UTC)
|
|
raw, adjusted, _ := s.applyDuration(in, 2, "weeks", "before", "DE", "UPC")
|
|
want := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
|
if !raw.Equal(want) {
|
|
t.Errorf("raw: got %s, want %s", raw, want)
|
|
}
|
|
if !adjusted.Equal(want) {
|
|
t.Errorf("adjusted: got %s, want %s", adjusted, want)
|
|
}
|
|
}
|
|
|
|
// Composite-rule test: R.198/R.213 "31d OR 20 working_days, whichever is longer".
|
|
// We hand-compute the two legs and pick max via the same logic as Calculate.
|
|
func TestComposite_R198_LongerLegWins(t *testing.T) {
|
|
s := &EventDeadlineService{holidays: NewHolidayService(nil)}
|
|
in := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC)
|
|
|
|
_, baseAdj, _ := s.applyDuration(in, 31, "days", "after", "DE", "UPC")
|
|
_, altAdj, _ := s.applyDuration(in, 20, "working_days", "after", "DE", "UPC")
|
|
|
|
// 31 calendar days from Thu 2026-04-30 = Sun 2026-05-31 → adjust to Mon 2026-06-01.
|
|
// 20 working days from Thu 2026-04-30 ≈ early June (skipping May 1 holiday + weekends).
|
|
// Hand-count: starts Apr 30 (Thu), wd1=May 4 (skip Fri holiday + weekend), wd2=May 5,
|
|
// wd3=May 6, wd4=May 7, wd5=May 8 (Fri), wd6=May 11 (Mon), wd7=May 12,
|
|
// wd8=May 13, wd9=May 14 (Christi Himmelfahrt skipped) → wd9=May 15, wd10=May 18,
|
|
// wd11=May 19, wd12=May 20, wd13=May 21, wd14=May 22, wd15=May 25 (Pfingstmontag skipped) → wd15=May 26,
|
|
// wd16=May 27, wd17=May 28, wd18=May 29, wd19=Jun 1, wd20=Jun 2.
|
|
// So altAdj = 2026-06-02.
|
|
wantAlt := time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)
|
|
if !altAdj.Equal(wantAlt) {
|
|
t.Fatalf("alt leg: got %s, want %s", altAdj, wantAlt)
|
|
}
|
|
wantBase := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
|
|
if !baseAdj.Equal(wantBase) {
|
|
t.Fatalf("base leg: got %s, want %s", baseAdj, wantBase)
|
|
}
|
|
|
|
// max(base, alt) = altAdj (Jun 2 > Jun 1) — working_days leg wins this case,
|
|
// matching the R.198 "whichever is longer" intent.
|
|
if !altAdj.After(baseAdj) {
|
|
t.Error("expected altAdj > baseAdj (working_days leg longer than 31d leg)")
|
|
}
|
|
}
|