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.
154 lines
4.5 KiB
Go
154 lines
4.5 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// ErrUnknownCourt is returned when a court ID is not found in paliad.courts.
|
|
var ErrUnknownCourt = errors.New("unknown court")
|
|
|
|
// Court is the deadline-computation slice of a court row from paliad.courts.
|
|
// The rich Gerichtsverzeichnis (addresses, languages, contacts) lives in the
|
|
// static catalog at internal/handlers/courts.go and is sibling to this entity,
|
|
// not master/replica — the static slice is what users browse, paliad.courts
|
|
// is what the deadline math joins against.
|
|
type Court struct {
|
|
ID string `db:"id" json:"id"`
|
|
Code string `db:"code" json:"code"`
|
|
NameDE string `db:"name_de" json:"nameDE"`
|
|
NameEN string `db:"name_en" json:"nameEN"`
|
|
Country string `db:"country" json:"country"` // ISO-3166 alpha-2
|
|
Regime *string `db:"regime" json:"regime,omitempty"` // 'UPC' | 'EPO' | NULL
|
|
CourtType string `db:"court_type" json:"courtType"`
|
|
ParentID *string `db:"parent_id" json:"parentId,omitempty"`
|
|
SortOrder int `db:"sort_order" json:"sortOrder"`
|
|
IsActive bool `db:"is_active" json:"isActive"`
|
|
}
|
|
|
|
// RegimeOrEmpty returns the court's regime as a plain string ("UPC" / "EPO"
|
|
// / "") so callers don't have to dereference Court.Regime.
|
|
func (c Court) RegimeOrEmpty() string {
|
|
if c.Regime == nil {
|
|
return ""
|
|
}
|
|
return *c.Regime
|
|
}
|
|
|
|
// CourtService loads + caches paliad.courts. Cache is built on first lookup
|
|
// and shared across subsequent calls; the catalog is small (~50 rows) and
|
|
// changes only via migration, so a single-shot load is fine.
|
|
type CourtService struct {
|
|
db *sqlx.DB
|
|
once sync.Once
|
|
mu sync.RWMutex
|
|
byID map[string]Court
|
|
all []Court
|
|
err error
|
|
}
|
|
|
|
// NewCourtService wires the service to the DB.
|
|
func NewCourtService(db *sqlx.DB) *CourtService {
|
|
return &CourtService{db: db}
|
|
}
|
|
|
|
func (s *CourtService) load() {
|
|
if s.db == nil {
|
|
s.err = fmt.Errorf("courts service: no DB configured")
|
|
return
|
|
}
|
|
var rows []Court
|
|
err := s.db.SelectContext(context.Background(), &rows, `
|
|
SELECT id, code, name_de, name_en, country, regime, court_type, parent_id, sort_order, is_active
|
|
FROM paliad.courts
|
|
WHERE is_active = true
|
|
ORDER BY sort_order, id`)
|
|
if err != nil {
|
|
s.err = fmt.Errorf("load courts: %w", err)
|
|
return
|
|
}
|
|
s.all = rows
|
|
s.byID = make(map[string]Court, len(rows))
|
|
for _, c := range rows {
|
|
s.byID[c.ID] = c
|
|
}
|
|
}
|
|
|
|
// ensureLoaded loads the catalog once. Subsequent calls are no-ops.
|
|
func (s *CourtService) ensureLoaded() error {
|
|
s.once.Do(s.load)
|
|
return s.err
|
|
}
|
|
|
|
// Lookup returns the court row for an ID. ErrUnknownCourt when not found.
|
|
func (s *CourtService) Lookup(id string) (Court, error) {
|
|
if id == "" {
|
|
return Court{}, ErrUnknownCourt
|
|
}
|
|
if err := s.ensureLoaded(); err != nil {
|
|
return Court{}, err
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
c, ok := s.byID[id]
|
|
if !ok {
|
|
return Court{}, ErrUnknownCourt
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// CountryRegime resolves a court ID to its (country, regime) tuple for
|
|
// holiday-calendar lookup. Empty courtID falls back to (defaultCountry,
|
|
// defaultRegime) so legacy callers without a court_id still get sensible
|
|
// behaviour. ErrUnknownCourt when courtID is non-empty and not in the table.
|
|
func (s *CourtService) CountryRegime(courtID, defaultCountry, defaultRegime string) (country, regime string, err error) {
|
|
if courtID == "" {
|
|
return defaultCountry, defaultRegime, nil
|
|
}
|
|
c, err := s.Lookup(courtID)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return c.Country, c.RegimeOrEmpty(), nil
|
|
}
|
|
|
|
// All returns the full active court list (read-only — caller must not
|
|
// mutate). Order: sort_order asc, id asc.
|
|
func (s *CourtService) All() ([]Court, error) {
|
|
if err := s.ensureLoaded(); err != nil {
|
|
return nil, err
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]Court, len(s.all))
|
|
copy(out, s.all)
|
|
return out, nil
|
|
}
|
|
|
|
// ByCourtType returns all active courts of a given type (e.g. "UPC-LD").
|
|
// Useful for the Fristenrechner court-picker UI.
|
|
func (s *CourtService) ByCourtType(courtType string) ([]Court, error) {
|
|
if err := s.ensureLoaded(); err != nil {
|
|
return nil, err
|
|
}
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
out := make([]Court, 0, 8)
|
|
for _, c := range s.all {
|
|
if c.CourtType == courtType {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// guard against nil-deref when courtID is supplied by an API caller and the
|
|
// service is wired. Not used internally; exposed so handlers can return
|
|
// 404-equivalents on bad input without leaking sql errors.
|
|
var _ = sql.ErrNoRows
|