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