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.
114 lines
3.8 KiB
Go
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,
|
|
})
|
|
}
|