Atomic extraction of the deadline-rule compute engine + types from
internal/services into a new pkg/litigationplanner package that paliad
+ youpc.org can both import. No behaviour change — every existing test
passes against the post-move shape.
Package contents (~1850 LoC):
- doc.go package docstring + reuse manifesto
- types.go Rule, ProceedingType, NullableJSON, AdjustmentReason,
HolidayDTO, CalcOptions, CalcRuleParams, Timeline,
TimelineEntry, RuleCalculation*, FristenrechnerType,
ProjectHint, sentinel errors
- catalog.go Catalog interface (proceeding + rule lookups)
- holidays.go HolidayCalendar interface
- courts.go CourtRegistry interface + DefaultsForJurisdiction +
country/regime constants
- expr.go EvalConditionExpr + HasConditionExpr +
ExtractFlagsFromExpr (jsonb gate evaluator)
- durations.go ApplyDuration + AddWorkingDays (pure compute)
- subtrack.go SubTrackRouting + LookupSubTrackRouting registry
- legal_source.go FormatLegalSourceDisplay + BuildLegalSourceURL
- proceeding_mapping.go MapLitigationToFristenrechner + code constants
(CodeUPCInfringement, CodeDEInfringementLG, ...)
- engine.go Calculate + CalculateRule + the trigger-event
branch + applyRuleOverrides (the big move)
paliad side (~1900 LoC net deletion):
- internal/services/fristenrechner.go shrinks from 1505 → ~290 lines
(thin paliad Catalog adapter + type aliases for back-compat).
- internal/models/models.go: DeadlineRule, ProceedingType, NullableJSON
become type aliases to litigationplanner.* — every sqlx scan and
every projection_service caller compiles unchanged.
- internal/services/holidays.go: AdjustmentReason + HolidayDTO become
aliases to lp.* (canonical definitions now in the package).
- internal/services/proceeding_mapping.go: rewritten as thin re-exports
of lp constants + helpers.
- internal/services/deadline_search_service.go: FormatLegalSourceDisplay
+ BuildLegalSourceURL replaced with delegating wrappers to lp.
Catalog interface satisfaction:
- DeadlineRuleService → paliadCatalog adapter (wraps the existing
service, replicates the original SELECT shapes).
- HolidayService → satisfies lp.HolidayCalendar directly (compile-
time assertion at end of fristenrechner.go).
- CourtService → satisfies lp.CourtRegistry directly.
Wire shape is byte-identical. JSON tags on Rule / ProceedingType /
Timeline / TimelineEntry / RuleCalculation match the historical
UIResponse / UIDeadline shape; the frontend reads the same bytes.
Slice B (Catalog interface + paliad loader cleanup) is folded into
this commit since Slice A already needs the interfaces to call
Calculate across the boundary. Slice C (embedded UPC snapshot +
generator) is the next coder shift; the Berufung unification m
called out lands in Slice B/C per head's brief.
Refs: docs/design-litigation-planner-2026-05-26.md
77 lines
2.7 KiB
Go
77 lines
2.7 KiB
Go
package litigationplanner
|
|
|
|
import "time"
|
|
|
|
// ApplyDuration is the unified date-arithmetic helper used by every
|
|
// calculator path (proceeding-tree, trigger-event, CalculateRule single-
|
|
// rule). Phase 3 Slice 4 (t-paliad-185) replaced the prior split
|
|
// between addDuration (proceeding-tree, no timing / working_days) and
|
|
// ApplyDurationOnCalendar (Pipeline-C, full support) with this single
|
|
// helper.
|
|
//
|
|
// Returns (raw, adjusted, didAdjust, reason):
|
|
//
|
|
// - raw: the date strictly implied by the rule before rollover.
|
|
// - adjusted: post-rollover for calendar units. 'working_days' lands
|
|
// on a working day by construction so raw == adjusted there.
|
|
// - didAdjust: true iff rollover moved the date.
|
|
// - reason: populated when didAdjust is true; nil otherwise.
|
|
//
|
|
// timing='before' negates the sign. timing='after' (or any other value
|
|
// including the empty string) keeps it positive — preserves the pre-
|
|
// Slice-4 behaviour for proceeding-tree rules whose Timing field is
|
|
// sometimes NULL (mig 003 defaults to 'after' but legacy callers pass
|
|
// r.Timing dereferenced).
|
|
func ApplyDuration(
|
|
base time.Time, value int, unit, timing, country, regime string, holidays HolidayCalendar,
|
|
) (raw, adjusted time.Time, didAdjust bool, reason *AdjustmentReason) {
|
|
sign := 1
|
|
if timing == "before" {
|
|
sign = -1
|
|
}
|
|
switch unit {
|
|
case "days":
|
|
raw = base.AddDate(0, 0, sign*value)
|
|
case "weeks":
|
|
raw = base.AddDate(0, 0, sign*value*7)
|
|
case "months":
|
|
raw = base.AddDate(0, sign*value, 0)
|
|
case "working_days":
|
|
raw = AddWorkingDays(base, sign*value, country, regime, holidays)
|
|
// Working-day arithmetic lands on a working day by construction
|
|
// — the per-step skip loop in AddWorkingDays already passes over
|
|
// weekends and holidays. No post-rollover required.
|
|
return raw, raw, false, nil
|
|
default:
|
|
raw = base
|
|
}
|
|
adjusted, _, didAdjust, reason = holidays.AdjustForNonWorkingDaysWithReason(raw, country, regime)
|
|
return raw, adjusted, didAdjust, reason
|
|
}
|
|
|
|
// AddWorkingDays advances from `from` by `n` working days, skipping
|
|
// weekends and holidays applicable to the given country/regime. Negative
|
|
// n walks backward. n=0 keeps the input date as-is (caller decides
|
|
// whether to roll forward via AdjustForNonWorkingDays).
|
|
//
|
|
// Bounded by an inner 30-step skip per advance — vacation runs in our
|
|
// holiday tables are < 14 consecutive days, so 30 is a safety margin.
|
|
func AddWorkingDays(from time.Time, n int, country, regime string, holidays HolidayCalendar) time.Time {
|
|
if n == 0 {
|
|
return from
|
|
}
|
|
step := 1
|
|
if n < 0 {
|
|
step = -1
|
|
n = -n
|
|
}
|
|
cur := from
|
|
for i := 0; i < n; i++ {
|
|
cur = cur.AddDate(0, 0, step)
|
|
for j := 0; j < 30 && holidays.IsNonWorkingDay(cur, country, regime); j++ {
|
|
cur = cur.AddDate(0, 0, step)
|
|
}
|
|
}
|
|
return cur
|
|
}
|