EventDeadlineService.Calculate now reads source rows from
paliad.deadline_rules directly (WHERE trigger_event_id IS NOT NULL),
joining via UUID instead of title_de string. The legacy SELECTs against
paliad.event_deadlines + paliad.event_deadline_rule_codes are gone.
Migration 092:
- Snapshots both legacy tables into _pre_092 audit anchors.
- Adds paliad.deadline_rules.rule_codes text[] and backfills the 72
multi-code citations from event_deadline_rule_codes via the
sequence_order = 1000 + ed.id convention from mig 085 (70 of 77
Pipeline-C deadlines carry codes; 7 are codeless).
- Hard assertion ties source-junction-row count to backfilled
text[]-element count — any sequence_order mismatch aborts the drop.
- Drops the mig 086 read-only trigger (orphan once event_deadlines
goes away).
- Drops paliad.event_deadlines + paliad.event_deadline_rule_codes.
- Final assertion: >=77 active deadline_rules with trigger_event_id
NOT NULL — Slice 3 corpus must not have collapsed.
- audit_reason wrapper at top so the deadline_rules UPDATE row-trigger
records the reason in deadline_rule_audit.
Verified via BEGIN..ROLLBACK against the live paliad DB: 72 codes
backfilled into 70 rule_codes arrays, multi-code rules (RoP.029.a +
RoP.030 for ed_id=6) preserve their ordering, composite rules
(combine_op=max) remain intact, both tables drop cleanly, all
assertions pass.
Parity test rebound to deadline_rules — independent computation still
re-runs applyDuration against raw column values for date/composite
parity. EventDeadlineResult.ID stays int64 via the sequence_order -
1000 convention so the public /api/tools/event-deadlines wire shape
is unchanged.
Phase 3 Slice 4 Step D (design §3.D, the last foundation slice).
Pure Go — no migrations. Collapses the proceeding-tree + Pipeline-C
calculators onto a single set of unified helpers + reads, all
without changing wire output.
Helpers (package-level in services/fristenrechner.go):
applyDuration(base, value, unit, timing, country, regime, holidays)
→ (raw, adjusted, didAdjust, reason)
Single source-of-truth for date arithmetic. Replaces:
- addDuration (proceeding-tree, no timing / working_days)
- applyDurationOnCalendar (Slice 3 Pipeline-C-only)
- EventDeadlineService.applyDuration / addWorkingDays methods
Handles: timing=before/after, units days/weeks/months/working_days,
weekend + holiday rollover for calendar units. working_days lands
on a working day by construction (no post-rollover).
evalConditionExpr(expr jsonb, conditionFlag []string, flags) bool
Long-form jsonb gate evaluator (design §2.4). Grammar:
leaf: {"flag":"X"}
AND: {"op":"and","args":[<n>...]}
OR: {"op":"or","args":[<n>...]}
NOT: {"op":"not","args":[<one>]}
NULL / empty / "null" → unconditional. Defensive fall-through
on malformed JSON / unknown ops (rule still renders — never
silently drop a deadline). Fallback to condition_flag
AND-semantics when expr is NULL but the legacy column is set
(defensive cover for any row Slice 2 missed).
wireFlagsFromPriority(priority) → (isMandatory, isOptional)
Derives the legacy wire pair from the unified priority enum:
mandatory → (T, F) — statutory must
optional → (T, T) — RoP.151 (opt-in, ☐ pre-unchecked)
recommended → (F, F) — situational filing
informational → (F, F) — never saves today
unknown → (T, F) — safe default
Slice 8 will swap the wire to emit priority directly.
Calculate (proceeding-tree) refactor:
- r.IsCourtSet column read direct, isCourtDeterminedRule() heuristic
function deleted. Slice 2 backfill (mig 082) wrote the column
using the exact heuristic predicate; column-read saves the
per-rule branch test at runtime.
- r.Priority drives the wire IsMandatory / IsOptional pair via
wireFlagsFromPriority. Read of r.IsMandatory / r.IsOptional
columns retained (compat-mode) but never decision-shaping.
- r.ConditionExpr drives the gate; condition_flag is the fallback.
- Added combine_op composite (max/min) branch for proceeding-tree
rules. No live Pipeline-A rules carry combine_op today (it's a
future-friendly column the rule editor will surface); the
branch is reachable but produces zero diffs on the current
corpus.
- timing=before + working_days now usable on proceeding-tree rules
via the unified applyDuration. No live Pipeline-A rules use them.
CalculateRule (single-rule card-click) refactor: same column reads
(IsCourtSet, ConditionExpr, Priority), unified applyDuration.
calculateByTriggerEvent (Pipeline C) refactor: switched to the
unified applyDuration; loses the redundant post-pick reason
recompute (applyDuration now returns reason directly).
EventDeadlineService.Calculate composite-note recompute now calls
the package-level applyDuration instead of the deleted method.
Frontend wire shape stays pixel-identical pre/post-Slice-4. The 17
condition_flag rules in the live corpus continue to gate via the
same (a) leaf or (b) AND-of-args evaluator branches mig 084
produced; jsonb path is exercised first, the array fallback
remains as defensive cover.
Phase 3 Slice 3 service-side rewire. EventDeadlineService.Calculate
now:
1. Looks up trigger event metadata (unchanged — the legacy response
shape still carries TriggerEvent + TriggerDate at the top level).
2. SELECTs source event_deadlines rows for the trigger to recover
(id, duration, alt_*, combine_op, notes_en) — the unified
UIResponse drops those fields. SELECT is still allowed by the
mig 086 read-only trigger; only writes are blocked.
3. Delegates the rule SELECT + math to FristenrechnerService.Calculate
with TriggerEventIDFilter set.
4. Merges the unified result with the source rows (join by Name =
title_de) to produce the legacy EventDeadlineResult shape with
ID, ruleCodes, isComposite, compositeNote intact.
5. Loads rule_codes from event_deadline_rule_codes (also still
readable) by source.id.
Public signature unchanged — /api/tools/event-deadlines callers see
no diff. The legacy applyDuration / addWorkingDays helpers stay on
EventDeadlineService for the pure-Go unit tests + the composite-note
leg-pick that the unified UIDeadline doesn't expose.
main.go wiring: NewEventDeadlineService gains the FristenrechnerService
dependency.
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.
Two adjacent i18n leaks in /tools/fristenrechner "Was kommt nach…" mode,
same pattern as t-paliad-112's deadline_rules fix but on event_deadlines:
A) title_de empty for all 70 rows. The DTO already falls back to title
silently, so DE locale rendered English titles ("Statement of Defence",
"Decision of the EPO", …). Backfilled via mig 035 with UPC RoP DE
terminology that matches the trigger_events.name_de translations from
mig 033, so the picker label and the deadline row read the same.
B) notes column carries English text on rows 50, 52, 70 (DE-named column
was DE-only in spec, but seeds slipped EN strings through). Mig 036
adds a parallel notes_en column following the t-112 mig 032 pattern,
copies the existing English into notes_en, and replaces notes with
proper DE for those three rows.
Render path:
- service: select notes_en, plumb through EventDeadlineResult.NotesEN
- frontend: getLang() === "en" ? (notesEN || notes) : notes (mirrors the
proceeding-tree timeline branch already in fristenrechner.ts)
Adds the second Fristenrechner mode (mirrored from youpc.org's deadline
calc): pick a UPC trigger event + date, see all deadlines that flow
from it. Coexists with the existing course-of-proceedings timeline mode
via a tab toggle on /tools/fristenrechner.
Backend:
- internal/services/event_deadline_service.go — EventDeadlineService.
ListTriggerEvents (alphabetical), Calculate (resolves all deadlines
flowing from a trigger). Routes through HolidayService for weekend +
holiday rollover. Honours the new working_days unit. Resolves
composite rules (alt_* + combine_op) by computing both legs and
picking max/min. Used by R.198/R.213 ("31d OR 20wd, whichever is
longer") imported in PR-1.
- internal/services/event_deadline_service_test.go — covers
addWorkingDays (forward, backward, zero, holiday-skip), composite
rule semantics, before-timing.
- internal/handlers/fristenrechner.go — two new endpoints:
GET /api/tools/trigger-events, POST /api/tools/event-deadlines.
- handlers.Services / dbServices: new EventDeadline / eventDeadline
field; wired in cmd/server/main.go from the same HolidayService.
Frontend:
- frontend/src/fristenrechner.tsx — tab strip + second wizard panel
(3 steps: trigger picker → date → flat result list).
- frontend/src/client/fristenrechner.ts — initEventMode wiring,
typeahead filter over the 102 trigger events, Calculate flow,
bilingual rendering, composite-rule labels, lang-change refresh.
- frontend/src/client/i18n.ts — 27 new keys (DE+EN) under
deadlines.mode.* and deadlines.event.* (incl. units, timing).
- frontend/src/styles/global.css — fristen-mode-tabs, mode-panel,
event-list, event-result-row visual style.
Working-day arithmetic detail: the new addWorkingDays helper steps
one day at a time and skips runs of non-working days (Sat/Sun + DE
federal + UPC vacations seeded via paliad.holidays). Day-zero is the
caller's job — addWorkingDays(0) returns the input unchanged so
callers can decide whether to roll forward via AdjustForNonWorkingDays.
Composite-rule resolution: when a row carries alt_duration_value +
alt_duration_unit + combine_op, Calculate computes both legs,
picks max/min, and surfaces a compositeNote like
"max(31 days, 20 working_days) → working_days leg" so the UI can
explain which leg won.
PR-3 will land Tier 1 bug fixes from the audit (CCR adaptive,
UPC_APP grounds anchoring, EP_GRANT priority, rule-code normalisation).