Files
paliad/internal/services/fristenrechner.go
mAi 989941c648
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
feat(litigationplanner): primary_party CHECK constraint + IsValidPrimaryParty helper (Slice B3, m/paliad#124 §18.3)
Tightens paliad.deadline_rules.primary_party from free-text to a CHECK
constraint over the canonical four-value vocab (claimant / defendant /
court / both). NULL stays valid for the 78 cross-cutting orphan
concept seeds (Wiedereinsetzung, Versäumnisurteil-Einspruch,
Schriftsatznachreichung, Weiterbehandlung) — they have no
proceeding_type_id binding so they're outside the calculator's path;
loosening the CHECK to "IS NULL OR IN (…)" keeps them valid without
backfill gymnastics.

Migration 135 (audit-first):
  - DO block RAISEs NOTICE for every non-conforming row + RAISEs
    EXCEPTION if any dirty rows exist (manual cleanup required).
    Live audit (Supabase, 2026-05-26 §18.0) confirmed zero dirty rows
    on the current corpus; the audit pass stays in the migration as
    safety against future drift.
  - ALTER TABLE … ADD CONSTRAINT deadline_rules_primary_party_chk
    CHECK (primary_party IS NULL OR primary_party IN
           ('claimant', 'defendant', 'court', 'both'))
  - Post-migration distribution NOTICE so the operator sees the
    final per-value count.
  - Down = DROP CONSTRAINT. No data revert needed.

Package additions (pkg/litigationplanner):
  - PrimaryParty* constants (PrimaryPartyClaimant / Defendant / Court
    / Both) + PrimaryParties[] ordered list + IsValidPrimaryParty(s)
    predicate. Empty string is "no value supplied" = valid (NULL maps
    to empty on the wire); non-empty must match one of the four
    canonical values.
  - Sibling unit tests (primary_party_test.go) pin the four-value
    vocab + the chip order + IsValidAppealTarget's matching shape.

Rule-editor validation hook (rule_editor_service.go):
  - Create() validates input.PrimaryParty before INSERT.
  - UpdateDraft() validates patch.PrimaryParty before UPDATE.
  - Both surface a user-friendly 400 with the canonical vocab listed
    instead of leaking the raw PG CHECK constraint-violation message.
  - Uses errors.Is(err, ErrInvalidInput) so handler 400 routing
    continues to work.

services/fristenrechner.go cleanup:
  - The B2-inlined isValidPartyForLookup helper is replaced with the
    canonical lp.IsValidPrimaryParty. No behaviour change.

No frontend changes — the rule-editor's primary_party UI already
constrains to the four values via a select; the validation hook is
defense-in-depth.

Audit:
  - go build + go test (incl. new lp unit tests) all green
  - Pre-migration audit confirmed: 26 claimant + 26 defendant + 38
    court + 63 both + 78 NULL = 231 total, all in canonical vocab
  - event_categories.party (text[] array, narrower semantic) is
    NOT touched in this migration per the design doc's
    "out of scope, separate follow-up" decision
2026-05-26 13:58:33 +02:00

607 lines
22 KiB
Go

package services
import (
"context"
"database/sql"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/google/uuid"
"mgit.msbls.de/m/paliad/internal/models"
lp "mgit.msbls.de/m/paliad/pkg/litigationplanner"
)
// FristenrechnerService renders the Paliad public Fristenrechner's
// response shape from DB-stored rules. Post-Slice-A (t-paliad-298) it
// is a thin adapter: the compute engine + types live in
// pkg/litigationplanner, and FristenrechnerService just wires the
// Postgres-backed Catalog + HolidayCalendar + CourtRegistry
// implementations and delegates Calculate / CalculateRule across the
// boundary.
//
// The package owns the wire shape (Timeline / TimelineEntry); paliad's
// historical aliases (UIResponse / UIDeadline) keep call-sites
// unchanged.
type FristenrechnerService struct {
rules *DeadlineRuleService
holidays *HolidayService
courts *CourtService
catalog lp.Catalog
}
// NewFristenrechnerService wires the service to its dependencies.
func NewFristenrechnerService(rules *DeadlineRuleService, holidays *HolidayService, courts *CourtService) *FristenrechnerService {
s := &FristenrechnerService{rules: rules, holidays: holidays, courts: courts}
s.catalog = &paliadCatalog{rules: rules}
return s
}
// Type aliases keep call-sites byte-identical with the pre-Slice-A
// shape. The wire JSON tags are owned by the package.
// (AdjustmentReason + HolidayDTO are aliased in holidays.go.)
type (
UIResponse = lp.Timeline
UIDeadline = lp.TimelineEntry
CalcOptions = lp.CalcOptions
CalcRuleParams = lp.CalcRuleParams
RuleCalculation = lp.RuleCalculation
RuleCalculationRule = lp.RuleCalculationRule
RuleCalculationProceeding = lp.RuleCalculationProceeding
SubTrackRouting = lp.SubTrackRouting
)
// Sentinel errors. Re-exported as package-level vars so handlers that
// errors.Is(..., services.ErrUnknownProceedingType) continue to work.
var (
ErrUnknownProceedingType = lp.ErrUnknownProceedingType
ErrUnknownRule = lp.ErrUnknownRule
)
// Calculate delegates to litigationplanner.Calculate with paliad's
// Postgres-backed Catalog / HolidayCalendar / CourtRegistry implementations.
func (s *FristenrechnerService) Calculate(ctx context.Context, proceedingCode, triggerDateStr string, opts CalcOptions) (*UIResponse, error) {
return lp.Calculate(ctx, proceedingCode, triggerDateStr, opts, s.catalog, s.holidays, s.courts)
}
// CalculateRule delegates to litigationplanner.CalculateRule. Distinct
// from Calculate: no parent-chain walk, no full-timeline rendering —
// just one date out.
func (s *FristenrechnerService) CalculateRule(ctx context.Context, params CalcRuleParams) (*RuleCalculation, error) {
return lp.CalculateRule(ctx, params, s.catalog, s.holidays, s.courts)
}
// ListFristenrechnerTypes returns the proceeding types that populate
// the Fristenrechner UI (category='fristenrechner'), ordered by
// sort_order. Stays on the service because the response is a paliad-
// specific surface (the wire shape FristenrechnerType is owned by the
// package but the SQL filter is paliad-side).
func (s *FristenrechnerService) ListFristenrechnerTypes(ctx context.Context) ([]lp.FristenrechnerType, error) {
rows, err := s.rules.db.QueryxContext(ctx, `
SELECT code, name, name_en, jurisdiction
FROM paliad.proceeding_types
WHERE category = 'fristenrechner' AND is_active = true
ORDER BY sort_order`)
if err != nil {
return nil, fmt.Errorf("list fristenrechner types: %w", err)
}
defer rows.Close()
var out []lp.FristenrechnerType
for rows.Next() {
var t lp.FristenrechnerType
var juris sql.NullString
if err := rows.Scan(&t.Code, &t.Name, &t.NameEN, &juris); err != nil {
return nil, err
}
if juris.Valid {
t.Group = juris.String
}
out = append(out, t)
}
return out, rows.Err()
}
// FristenrechnerType is paliad's local alias for lp.FristenrechnerType
// so historical call-sites (services.FristenrechnerType) keep working.
type FristenrechnerType = lp.FristenrechnerType
// ---------------------------------------------------------------------
// paliadCatalog is the paliad-side litigationplanner.Catalog adapter.
// Wraps DeadlineRuleService to expose proceeding + rule lookups against
// paliad.proceeding_types + paliad.deadline_rules.
// ---------------------------------------------------------------------
type paliadCatalog struct {
rules *DeadlineRuleService
}
// proceedingTypeColumns is canonically defined in
// deadline_rule_service.go; the catalog adapter reuses it via the
// shared package-level const.
// LoadProceeding returns the proceeding-type metadata + rules. The
// ProjectHint is currently ignored on paliad's side (per m's 2026-05-26
// decision dropping the Slice E user-authored rules); kept on the
// interface for forward-compat.
func (c *paliadCatalog) LoadProceeding(ctx context.Context, code string, _ lp.ProjectHint) (*models.ProceedingType, []models.DeadlineRule, error) {
var pt models.ProceedingType
err := c.rules.db.GetContext(ctx, &pt,
`SELECT `+proceedingTypeColumns+`
FROM paliad.proceeding_types
WHERE code = $1 AND is_active = true`, code)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, lp.ErrUnknownProceedingType
}
if err != nil {
return nil, nil, fmt.Errorf("resolve proceeding %q: %w", code, err)
}
rules, err := c.rules.List(ctx, &pt.ID)
if err != nil {
return nil, nil, err
}
return &pt, rules, nil
}
// LoadProceedingByID is the resolver for a rule's parent proceeding.
func (c *paliadCatalog) LoadProceedingByID(ctx context.Context, id int) (*models.ProceedingType, error) {
var pt models.ProceedingType
err := c.rules.db.GetContext(ctx, &pt,
`SELECT `+proceedingTypeColumns+`
FROM paliad.proceeding_types
WHERE id = $1`, id)
if errors.Is(err, sql.ErrNoRows) {
return nil, lp.ErrUnknownProceedingType
}
if err != nil {
return nil, fmt.Errorf("resolve proceeding by id %d: %w", id, err)
}
return &pt, nil
}
// LoadRuleByID resolves a rule UUID to the rule row.
func (c *paliadCatalog) LoadRuleByID(ctx context.Context, ruleID string) (*models.DeadlineRule, error) {
var rule models.DeadlineRule
err := c.rules.db.GetContext(ctx, &rule,
`SELECT `+ruleColumns+`
FROM paliad.deadline_rules
WHERE id = $1 AND is_active = true`, ruleID)
if errors.Is(err, sql.ErrNoRows) {
return nil, lp.ErrUnknownRule
}
if err != nil {
return nil, fmt.Errorf("resolve rule by id %q: %w", ruleID, err)
}
if err := c.rules.hydrateConceptDefaultEventTypes(ctx, []models.DeadlineRule{rule}); err != nil {
return nil, err
}
return &rule, nil
}
// LoadRuleByCode resolves a rule by (proceedingCode, submissionCode)
// + returns the parent proceeding for use in the response identity.
func (c *paliadCatalog) LoadRuleByCode(ctx context.Context, proceedingCode, submissionCode string) (*models.DeadlineRule, *models.ProceedingType, error) {
var pt models.ProceedingType
err := c.rules.db.GetContext(ctx, &pt,
`SELECT `+proceedingTypeColumns+`
FROM paliad.proceeding_types
WHERE code = $1 AND is_active = true`, proceedingCode)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, lp.ErrUnknownProceedingType
}
if err != nil {
return nil, nil, fmt.Errorf("resolve proceeding %q: %w", proceedingCode, err)
}
var rule models.DeadlineRule
err = c.rules.db.GetContext(ctx, &rule,
`SELECT `+ruleColumns+`
FROM paliad.deadline_rules
WHERE proceeding_type_id = $1 AND submission_code = $2 AND is_active = true`,
pt.ID, submissionCode)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil, lp.ErrUnknownRule
}
if err != nil {
return nil, nil, fmt.Errorf("resolve rule %q in %q: %w", submissionCode, proceedingCode, err)
}
if err := c.rules.hydrateConceptDefaultEventTypes(ctx, []models.DeadlineRule{rule}); err != nil {
return nil, nil, err
}
return &rule, &pt, nil
}
// LoadRulesByTriggerEvent lists Pipeline-C trigger-event-rooted rules.
func (c *paliadCatalog) LoadRulesByTriggerEvent(ctx context.Context, triggerEventID int64) ([]models.DeadlineRule, error) {
return c.rules.ListByTriggerEvent(ctx, triggerEventID)
}
// LoadTriggerEventsByIDs bulk-loads paliad.trigger_events rows for
// the conditional-label override (t-paliad-294 / m/paliad#126).
func (c *paliadCatalog) LoadTriggerEventsByIDs(ctx context.Context, ids []int64) (map[int64]models.TriggerEvent, error) {
return c.rules.LoadTriggerEventsByIDs(ctx, ids)
}
// LookupEvents queries paliad.deadline_rules for rules matching the
// requested axes, then walks the parent_id graph in Go to honour the
// requested depth. Slice B2 (m/paliad#124 §18.2).
//
// Filter axes apply at the SQL layer:
// - Jurisdiction: WHERE paliad.proceeding_types.jurisdiction = $X
// - ProceedingTypeID: WHERE deadline_rules.proceeding_type_id = $X
// - Party: WHERE deadline_rules.primary_party = $X
// - EventCategoryID: EXISTS subquery on
// paliad.event_category_concepts joined via concept_id
// - AppealTarget: WHERE $X = ANY(deadline_rules.applies_to_target)
//
// Depth is applied post-fetch: for EventLookupDepthNext, anchor rules
// (matched directly) are returned at depth=1 + their immediate
// children (parent_id IN matched-set) at depth=2. For
// EventLookupDepthAllFollowing, the parent_id walk continues
// recursively. The walk stays within the per-proceeding rule set
// (cross-proceeding spawn following is handled by the engine, not by
// LookupEvents).
//
// "published + active" gate: lifecycle_state='published' AND
// is_active=true (matches LoadProceeding's WHERE clause).
func (c *paliadCatalog) LookupEvents(ctx context.Context, axes lp.EventLookupAxes, depth lp.EventLookupDepth) ([]lp.EventMatch, error) {
// Validate axis values up front; unknown values fall through as
// "no filter on this axis" so a stale frontend chip doesn't
// silently drop the entire result set.
jurisdiction := axes.Jurisdiction
if jurisdiction != "" && jurisdiction != "UPC" && jurisdiction != "DE" &&
jurisdiction != "EPA" && jurisdiction != "DPMA" {
jurisdiction = ""
}
party := axes.Party
if party != "" && !lp.IsValidPrimaryParty(party) {
party = ""
}
appealTarget := axes.AppealTarget
if appealTarget != "" && !lp.IsValidAppealTarget(appealTarget) {
appealTarget = ""
}
// Build the WHERE clause progressively. Each axis adds a $N
// placeholder + appends to the args slice.
where := []string{
"dr.is_active = true",
"dr.lifecycle_state = 'published'",
"pt.is_active = true",
}
args := []any{}
add := func(clause string, val any) {
args = append(args, val)
where = append(where, fmt.Sprintf(clause, len(args)))
}
if jurisdiction != "" {
add("pt.jurisdiction = $%d", jurisdiction)
}
if axes.ProceedingTypeID != nil {
add("dr.proceeding_type_id = $%d", *axes.ProceedingTypeID)
}
if party != "" {
add("dr.primary_party = $%d", party)
}
if axes.EventCategoryID != nil {
// Junction-table EXISTS: the rule's concept_id must appear in
// paliad.event_category_concepts with the matching
// event_category_id.
add(`EXISTS (
SELECT 1 FROM paliad.event_category_concepts ecc
WHERE ecc.event_category_id = $%d
AND ecc.concept_id = dr.concept_id
)`, *axes.EventCategoryID)
}
if appealTarget != "" {
add("$%d = ANY(dr.applies_to_target)", appealTarget)
}
query := `
SELECT ` + ruleColumns + `,
pt.id AS pt_id, pt.code AS pt_code, pt.name AS pt_name,
pt.name_en AS pt_name_en, pt.description AS pt_description,
pt.jurisdiction AS pt_jurisdiction, pt.category AS pt_category,
pt.default_color AS pt_default_color, pt.sort_order AS pt_sort_order,
pt.is_active AS pt_is_active,
pt.trigger_event_label_de AS pt_trigger_event_label_de,
pt.trigger_event_label_en AS pt_trigger_event_label_en,
pt.appeal_target AS pt_appeal_target
FROM paliad.deadline_rules dr
JOIN paliad.proceeding_types pt ON pt.id = dr.proceeding_type_id
WHERE ` + strings.Join(where, "\n AND ") + `
ORDER BY dr.proceeding_type_id, dr.sequence_order`
var rows []lookupEventsRow
if err := c.rules.db.SelectContext(ctx, &rows, query, args...); err != nil {
return nil, fmt.Errorf("lookup events: %w", err)
}
if len(rows) == 0 {
return []lp.EventMatch{}, nil
}
// matchedIDs is the set of rule IDs that satisfied the axes (the
// "anchor" matches at depth=1). For EventLookupDepthNext we add
// their direct children. For EventLookupDepthAllFollowing we walk
// the parent_id chain transitively.
matchedIDs := make(map[uuid.UUID]bool, len(rows))
anchorMatch := make(map[uuid.UUID]bool, len(rows))
rowByID := make(map[uuid.UUID]lookupEventsRow, len(rows))
for _, r := range rows {
matchedIDs[r.ID] = true
anchorMatch[r.ID] = true
rowByID[r.ID] = r
}
// For depth control we need the full per-proceeding rule corpus
// (so we can find children whose parent_id ∈ matchedIDs even when
// those children don't match the axes themselves). Skip this when
// depth is empty (treated as "anchors only" — undocumented but
// useful as a degenerate case).
expandFromCorpus := func(corpus []models.DeadlineRule, joinedFor map[int]lookupEventsRow) {
// We loop until no new descendants are added (transitive
// closure under parent_id ∈ matchedIDs). EventLookupDepthNext
// stops after one pass; AllFollowing iterates to fixpoint.
for {
grew := false
for _, r := range corpus {
if r.ParentID == nil {
continue
}
if !matchedIDs[*r.ParentID] {
continue
}
if matchedIDs[r.ID] {
continue
}
matchedIDs[r.ID] = true
j := joinedFor[*r.ProceedingTypeID]
rowByID[r.ID] = lookupEventsRow{
DeadlineRule: r,
PTID: j.PTID,
PTCode: j.PTCode,
PTName: j.PTName,
PTNameEN: j.PTNameEN,
PTDescription: j.PTDescription,
PTJurisdiction: j.PTJurisdiction,
PTCategory: j.PTCategory,
PTDefaultColor: j.PTDefaultColor,
PTSortOrder: j.PTSortOrder,
PTIsActive: j.PTIsActive,
PTTriggerEventLabelDE: j.PTTriggerEventLabelDE,
PTTriggerEventLabelEN: j.PTTriggerEventLabelEN,
PTAppealTarget: j.PTAppealTarget,
}
grew = true
}
if !grew || depth == lp.EventLookupDepthNext {
break
}
}
}
if depth == lp.EventLookupDepthNext || depth == lp.EventLookupDepthAllFollowing {
// Load the proceeding-scoped corpus for every proceeding_type
// that appeared in the anchor set. The walk needs full
// visibility into each proceeding's rule tree so it can
// resolve parent_id chains.
procIDs := make(map[int]struct{})
joinedFor := make(map[int]lookupEventsRow)
for _, r := range rows {
procIDs[r.PTID] = struct{}{}
if _, ok := joinedFor[r.PTID]; !ok {
joinedFor[r.PTID] = r
}
}
for ptID := range procIDs {
corpus, err := c.rules.List(ctx, &ptID)
if err != nil {
return nil, fmt.Errorf("lookup events: load proceeding %d corpus: %w", ptID, err)
}
expandFromCorpus(corpus, joinedFor)
}
}
// depths[id] = sequence-depth from the closest anchor ancestor.
// Anchors are depth=1; their direct children are depth=2; etc.
depths := computeDepths(rowByID, anchorMatch)
// Compose the result slice ordered by (PTID, sequence_order).
type withKey struct {
match lp.EventMatch
key int64
}
items := make([]withKey, 0, len(matchedIDs))
for id := range matchedIDs {
r := rowByID[id]
var parentRuleID *uuid.UUID
if r.ParentID != nil && matchedIDs[*r.ParentID] {
p := *r.ParentID
parentRuleID = &p
}
items = append(items, withKey{
match: lp.EventMatch{
Rule: r.DeadlineRule,
ProceedingType: lp.ProceedingType{
ID: r.PTID,
Code: r.PTCode,
Name: r.PTName,
NameEN: r.PTNameEN,
Description: r.PTDescription,
Jurisdiction: r.PTJurisdiction,
Category: r.PTCategory,
DefaultColor: r.PTDefaultColor,
SortOrder: r.PTSortOrder,
IsActive: r.PTIsActive,
TriggerEventLabelDE: r.PTTriggerEventLabelDE,
TriggerEventLabelEN: r.PTTriggerEventLabelEN,
AppealTarget: r.PTAppealTarget,
},
Priority: r.Priority,
DepthFromAnchor: depths[id],
ParentRuleID: parentRuleID,
},
key: int64(r.PTID)*1_000_000 + int64(r.SequenceOrder),
})
}
sort.Slice(items, func(a, b int) bool { return items[a].key < items[b].key })
out := make([]lp.EventMatch, len(items))
for i, it := range items {
out[i] = it.match
}
return out, nil
}
// lookupEventsRow is the joined SELECT shape for LookupEvents — one
// deadline_rules row plus its proceeding_types parent columns. Kept
// at package scope so computeDepths can reference it.
type lookupEventsRow struct {
models.DeadlineRule
PTID int `db:"pt_id"`
PTCode string `db:"pt_code"`
PTName string `db:"pt_name"`
PTNameEN string `db:"pt_name_en"`
PTDescription *string `db:"pt_description"`
PTJurisdiction *string `db:"pt_jurisdiction"`
PTCategory *string `db:"pt_category"`
PTDefaultColor string `db:"pt_default_color"`
PTSortOrder int `db:"pt_sort_order"`
PTIsActive bool `db:"pt_is_active"`
PTTriggerEventLabelDE *string `db:"pt_trigger_event_label_de"`
PTTriggerEventLabelEN *string `db:"pt_trigger_event_label_en"`
PTAppealTarget *string `db:"pt_appeal_target"`
}
// computeDepths walks from each rule up the parent_id chain until it
// hits an anchor match (or runs out). The depth of the anchor is 1;
// each step away adds one. Rules whose entire chain has no anchor
// (defensive — shouldn't happen given the expand-from-corpus walk
// only adds children of matched parents) get depth=1.
//
// Iteration-bounded by the corpus size to prevent infinite loops on
// hypothetical parent_id cycles (mig 134 + the schema CHECKs already
// preclude cycles, but the bound is cheap insurance).
func computeDepths(
rowByID map[uuid.UUID]lookupEventsRow,
anchors map[uuid.UUID]bool,
) map[uuid.UUID]int {
depths := make(map[uuid.UUID]int, len(rowByID))
for id := range rowByID {
if anchors[id] {
depths[id] = 1
continue
}
// Walk parents until we find an anchor or run out.
d := 1
cur := id
maxIter := len(rowByID) + 1
for i := 0; i < maxIter; i++ {
r := rowByID[cur]
if r.ParentID == nil {
break
}
d++
cur = *r.ParentID
if anchors[cur] {
break
}
}
depths[id] = d
}
return depths
}
// _ proves paliadCatalog satisfies lp.Catalog at compile time.
var _ lp.Catalog = (*paliadCatalog)(nil)
// Ensure HolidayService satisfies lp.HolidayCalendar at compile time.
// HolidayService.AdjustForNonWorkingDaysWithReason returns the
// AdjustmentReason via paliad's internal type — since lp.AdjustmentReason
// is now the canonical definition and AdjustmentReason inside services
// is aliased to it, the signatures align verbatim.
var _ lp.HolidayCalendar = (*HolidayService)(nil)
// Ensure CourtService satisfies lp.CourtRegistry at compile time.
var _ lp.CourtRegistry = (*CourtService)(nil)
// ---------------------------------------------------------------------
// Helpers used by sibling services (event_trigger_service,
// event_deadline_service). Re-exported as thin wrappers so the existing
// call-sites in those services continue to compile without an import
// rewrite. A future slice can collapse them onto direct lp.* imports.
// ---------------------------------------------------------------------
// applyDuration delegates to litigationplanner.ApplyDuration.
func applyDuration(base time.Time, value int, unit, timing, country, regime string, holidays *HolidayService) (raw, adjusted time.Time, didAdjust bool, reason *AdjustmentReason) {
return lp.ApplyDuration(base, value, unit, timing, country, regime, holidays)
}
// addWorkingDays delegates to litigationplanner.AddWorkingDays.
//
//nolint:unused // referenced for forward-compat with sibling services
func addWorkingDays(from time.Time, n int, country, regime string, holidays *HolidayService) time.Time {
return lp.AddWorkingDays(from, n, country, regime, holidays)
}
// evalConditionExpr delegates to litigationplanner.EvalConditionExpr.
func evalConditionExpr(expr []byte, flags map[string]struct{}) bool {
return lp.EvalConditionExpr(expr, flags)
}
// hasConditionExpr delegates to litigationplanner.HasConditionExpr.
func hasConditionExpr(expr models.NullableJSON) bool {
return lp.HasConditionExpr(expr)
}
// extractFlagsFromExpr delegates to litigationplanner.ExtractFlagsFromExpr.
//
//nolint:unused // retained for sibling services that may want it
func extractFlagsFromExpr(expr models.NullableJSON) []string {
return lp.ExtractFlagsFromExpr(expr)
}
// allFlagsSet delegates to litigationplanner.AllFlagsSet. Retained for
// the paliad-side test suite that asserts the helper's contract.
func allFlagsSet(required []string, set map[string]struct{}) bool {
return lp.AllFlagsSet(required, set)
}
// wireFlagsFromPriority delegates to
// litigationplanner.WireFlagsFromPriority. Retained for the paliad-side
// test suite that asserts the priority → (isMandatory, isOptional)
// mapping.
func wireFlagsFromPriority(priority string) (isMandatory, isOptional bool) {
return lp.WireFlagsFromPriority(priority)
}
// sortDeadlinesByDurationWithinTriggerGroup is the paliad-side wrapper
// retained for the t-paliad-296 sort tests. Delegates to the
// package-internal sort over the lp.TimelineEntry shape — which is
// just an alias for UIDeadline, so callers pass []UIDeadline directly.
func sortDeadlinesByDurationWithinTriggerGroup(
deadlines []UIDeadline,
ruleByID map[uuid.UUID]models.DeadlineRule,
) {
lp.SortDeadlinesByDurationWithinTriggerGroup(deadlines, ruleByID)
}
// DefaultsForJurisdiction delegates to
// litigationplanner.DefaultsForJurisdiction. Public re-export so
// handlers (deadline_rules_db.go) can keep using
// services.DefaultsForJurisdiction without an import-rewrite.
func DefaultsForJurisdiction(jurisdiction *string) (country, regime string) {
return lp.DefaultsForJurisdiction(jurisdiction)
}
// applyRuleOverrides delegates to litigationplanner.ApplyRuleOverrides.
//
//nolint:unused // retained for sibling services that may want it
func applyRuleOverrides(src, overrides []models.DeadlineRule) []models.DeadlineRule {
return lp.ApplyRuleOverrides(src, overrides)
}