Files
paliad/internal/handlers/fristenrechner_search.go
mAi 7ea415145f
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(fristenrechner): Slice S1 — backend ?kind=events + /follow-ups (m/paliad#146)
Two additive endpoints behind the Fristenrechner overhaul (design
§6.1 + §6.2 in docs/design-fristenrechner-overhaul-2026-05-26.md):

1. GET /api/tools/fristenrechner/search?kind=events — returns
   procedural_events rows directly (not aggregated concept-cards),
   one hit per (event × proceeding_type) tuple. Trigram-ranked
   against name / name_en / code. Filters: jurisdiction, proc,
   event_kind, party. Powers Mode A's result list and Mode B's R4
   landing chips. Default search shape unchanged.

2. GET /api/tools/fristenrechner/follow-ups?event=...&trigger_date=...
   — given a trigger event (by code or uuid) + date, returns the
   immediate follow-up sequencing rules with computed due dates
   via litigationplanner.CalculateRule. Each row carries priority /
   primary_party / is_court_set / is_spawn / has_condition / legal
   source / spawn target so the result view can group into
   Mandatory / Recommended / Optional / Conditional with the
   SPAWNED badge. party=claimant|defendant filters keep "both"
   rules visible.

No schema changes — unified sequencing_rules already has every
column needed. Live-DB tests cover the SoC follow-up shape, party
narrowing, jurisdiction + event_kind filters, and the unknown-
event sentinel.
2026-05-26 22:01:10 +02:00

130 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"net/http"
"strconv"
"strings"
"mgit.msbls.de/m/paliad/internal/services"
)
// GET /api/tools/fristenrechner/search — unified search across the
// Fristenrechner concept layer (t-paliad-131 Phase C, t-paliad-133 v3
// extension). Returns at most `limit` concept cards, each with its
// proceeding pills.
//
// Query params:
// q - free-text search (trigram + alias)
// party - filter by effective_party
// proc - filter by proceeding_type code
// source - filter by legal_source prefix
// event_category_slug - v3 B1 narrowing; only concepts reachable
// from this taxonomy node and its descendants
// appear. Empty q is allowed when this is set
// (browse mode).
// browse - "all" enables v3 B1 entry mode: returns
// every concept mapped to any leaf of the
// decision tree (no narrowing, no query).
// Ignored when q is non-empty.
// forum - comma-separated v3 forum-bucket slugs
// (upc_cfi, upc_coa, de_lg, de_olg, de_bgh,
// de_bpatg, epa_grant, epa_opp, epa_appeal,
// dpma). Trigger pills bypass this filter.
// limit - max cards (default 12, max 30; in browse
// modes default 200, max 500)
// kind - "events" switches to the events-shape
// response (Fristenrechner overhaul S1,
// design §6.1). The default concept-card
// shape is unchanged when kind is empty.
//
// Returns an empty cards array (not 400) when q is empty — that lets
// the frontend boot the search input without a server round-trip.
func handleFristenrechnerSearch(w http.ResponseWriter, r *http.Request) {
if dbSvc == nil || dbSvc.deadlineSearch == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
"error": "Suche vorübergehend nicht verfügbar (keine Datenbank).",
})
return
}
if r.URL.Query().Get("kind") == "events" {
handleFristenrechnerSearchEvents(w, r)
return
}
q := r.URL.Query().Get("q")
opts := services.SearchOptions{
Party: r.URL.Query().Get("party"),
Proc: r.URL.Query().Get("proc"),
Source: r.URL.Query().Get("source"),
EventCategorySlug: r.URL.Query().Get("event_category_slug"),
Forums: parseCSV(r.URL.Query().Get("forum")),
BrowseAll: r.URL.Query().Get("browse") == "all",
Limit: parseLimit(r.URL.Query().Get("limit")),
}
resp, err := dbSvc.deadlineSearch.Search(r.Context(), q, opts)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Suche fehlgeschlagen: " + err.Error()})
return
}
writeJSON(w, http.StatusOK, resp)
}
// handleFristenrechnerSearchEvents serves the ?kind=events shape of
// /api/tools/fristenrechner/search (overhaul S1, design §6.1). Returns
// one hit per (procedural_event × proceeding_type) tuple, with a
// follow-up count and a trigram similarity score.
//
// Query params (additive to the legacy search params):
// q - free-text search against name / name_en / code
// jurisdiction - "UPC" | "DE" | "EPA" | "DPMA"
// proc - proceeding_type code
// event_kind - "filing" | "hearing" | "decision" | "order"
// party - primary_party of the anchor rule
// limit - max hits (default 50, max 200)
func handleFristenrechnerSearchEvents(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
opts := services.EventSearchOptions{
Jurisdiction: strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("jurisdiction"))),
ProceedingTypeCode: r.URL.Query().Get("proc"),
EventKind: r.URL.Query().Get("event_kind"),
PrimaryParty: r.URL.Query().Get("party"),
Limit: parseLimit(r.URL.Query().Get("limit")),
}
resp, err := dbSvc.deadlineSearch.SearchEvents(r.Context(), q, opts)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "Ereignis-Suche fehlgeschlagen: " + err.Error()})
return
}
writeJSON(w, http.StatusOK, resp)
}
// parseCSV splits a comma-separated query-string value into a slice of
// trimmed non-empty entries. Empty input → nil.
func parseCSV(raw string) []string {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
if len(out) == 0 {
return nil
}
return out
}
func parseLimit(raw string) int {
if raw == "" {
return 0
}
n, err := strconv.Atoi(raw)
if err != nil || n < 0 {
return 0
}
return n
}