Files
paliad/internal/handlers/deadline_rules_db.go
mAi 5b81f2159e feat(t-paliad-186): service guard + ?category filter
Phase 3 Slice 5 Go-side: ErrInvalidProceedingTypeCategory typed
error + service-layer validation + handler-level mapping +
listing-side filter.

  - services.ErrInvalidProceedingTypeCategory: typed error so
    handlers can map to a 400 with a bilingual user-facing message
    distinct from generic ErrInvalidInput.

  - ProjectService.validateProceedingTypeCategory: looks up the
    referenced proceeding_types.category and rejects with the typed
    error if it's not 'fristenrechner'. Called from both Create and
    Update before any DB write.

  - DeadlineRuleService.ListProceedingTypesByCategory: extends the
    existing ListProceedingTypes with an optional category filter.
    Empty category passes through (legacy callers unaffected).

  - GET /api/proceeding-types-db?category=<value>: handler reads the
    query param and forwards it to the service. The project-create
    / project-edit pickers pass 'fristenrechner' so users never see
    retired litigation codes.

  - writeServiceError: maps ErrInvalidProceedingTypeCategory to
    HTTP 400 with a bilingual message ("Verfahrenstyp muss ein
    Fristenrechner-Typ sein / proceeding type must be a
    Fristenrechner type"). Distinct from generic ErrInvalidInput so
    the frontend can show a more helpful hint.

Defence-in-depth chain: frontend picker filter → service-layer
validation → DB trigger (mig 088). Each backstops the next.
2026-05-15 01:01:28 +02:00

114 lines
3.8 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"strconv"
"time"
"mgit.msbls.de/m/paliad/internal/services"
)
// GET /api/deadline-rules?proceeding_type_id=N
//
// Lists deadline rules from the DB, optionally filtered by proceeding type.
// Returns 503 if the DB is not configured.
func handleListDeadlineRules(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
var ptIDPtr *int
if raw := r.URL.Query().Get("proceeding_type_id"); raw != "" {
ptID, err := strconv.Atoi(raw)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid proceeding_type_id"})
return
}
ptIDPtr = &ptID
}
rules, err := dbSvc.rules.List(r.Context(), ptIDPtr)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list rules"})
return
}
writeJSON(w, http.StatusOK, rules)
}
// GET /api/proceeding-types-db?category=<value>
//
// Lists active proceeding types from the DB. Optional `category` query
// param filters the result set (e.g. ?category=fristenrechner is the
// shape the project-create / project-edit pickers use after Phase 3
// Slice 5 — design §3.F + m's Q2 ruling restricts project-binding to
// fristenrechner-category codes). Empty / missing param returns every
// active row.
//
// (Distinct route name from the existing in-memory /api/tools/proceeding-types
// endpoint to avoid path conflicts during the Phase B → Phase C transition.)
func handleListProceedingTypesDB(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
category := r.URL.Query().Get("category")
types, err := dbSvc.rules.ListProceedingTypesByCategory(r.Context(), category)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list proceeding types"})
return
}
writeJSON(w, http.StatusOK, types)
}
// POST /api/deadlines/calculate
//
// Body: { "proceeding_type": "INF", "trigger_date": "2026-04-15" }
// Calculates all deadlines for the proceeding type's rule tree, applying
// holiday/weekend adjustment via the DB-backed HolidayService.
//
// Lives at /api/deadlines/calculate (vs the existing /api/tools/fristenrechner
// which uses the in-memory rule tree). Phase C swaps the Fristenrechner UI
// to this endpoint, then deletes the in-memory rule tree.
func handleCalculateDeadlines(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
var input struct {
ProceedingType string `json:"proceeding_type"`
TriggerDate string `json:"trigger_date"`
CourtID string `json:"court_id"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
if input.ProceedingType == "" || input.TriggerDate == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "proceeding_type and trigger_date required"})
return
}
triggerDate, err := time.Parse("2006-01-02", input.TriggerDate)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "trigger_date must be YYYY-MM-DD"})
return
}
rules, pt, err := dbSvc.rules.GetFullTimeline(r.Context(), input.ProceedingType)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unknown proceeding type"})
return
}
defaultCountry, defaultRegime := services.DefaultsForJurisdiction(pt.Jurisdiction)
country, regime, err := dbSvc.courts.CountryRegime(input.CourtID, defaultCountry, defaultRegime)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "unknown court_id"})
return
}
results := dbSvc.calc.CalculateFromRules(triggerDate, rules, country, regime)
writeJSON(w, http.StatusOK, map[string]any{
"proceeding_type": pt.Code,
"proceeding_name": pt.Name,
"trigger_date": input.TriggerDate,
"deadlines": results,
})
}