m/paliad#96 — slice A engine + slice B engine wired together (per m's Q4 bundling decision in §11 of the design doc). Engine (internal/services/fristenrechner.go): - CalcOptions gains PerCardAppellant map, SkipRules set, IncludeCCRFor set. All three keyed by paliad.deadline_rules.submission_code (same key AnchorOverrides uses). - UIDeadline gains AppellantContext (per-decision pick that propagates to descendants via parent_id chain) + ChoicesOffered (passes the jsonb through to the frontend so the caret renders). - Calculate honours all three: * IncludeCCRFor non-empty → append with_ccr to flag set before gate evaluation (v1 simplification documented in CalcOptions comment; correct for single-CCR-entry-point proceedings). * SkipRules suppression via submission_code match AND parent_id cascade (descendants suppress too — one-pass walk in sequence_order). * AppellantContext: each rule with its own per-card pick stamps its UUID; descendants inherit via parent_id lookup; "" = no override. HTTP: - /api/projects/{id}/event-choices GET / PUT / DELETE — full CRUD with visibility gate, audit-logged via paliad.system_audit_log. - POST /api/tools/fristenrechner accepts either projectId (server pulls choices from project_event_choices) OR inline perCardChoices (unbound /tools/verfahrensablauf surface). Inline wins when both. Services wiring: - EventChoiceService instantiated in cmd/server/main.go; threaded into handlers.dbServices.eventChoice.
114 lines
3.2 KiB
Go
114 lines
3.2 KiB
Go
package handlers
|
|
|
|
// HTTP handlers for paliad.project_event_choices (t-paliad-265 / m/paliad#96).
|
|
//
|
|
// Three endpoints:
|
|
// GET /api/projects/{id}/event-choices → list
|
|
// PUT /api/projects/{id}/event-choices → upsert one
|
|
// DELETE /api/projects/{id}/event-choices/{submission_code}/{choice_kind}
|
|
//
|
|
// All three gated by visibility on the project (paliad.can_see_project)
|
|
// via EventChoiceService.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/services"
|
|
)
|
|
|
|
// GET /api/projects/{id}/event-choices
|
|
func handleListProjectEventChoices(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if dbSvc.eventChoice == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "event-choice service not configured"})
|
|
return
|
|
}
|
|
projectID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
|
|
return
|
|
}
|
|
rows, err := dbSvc.eventChoice.ListForProject(r.Context(), uid, projectID)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, rows)
|
|
}
|
|
|
|
// PUT /api/projects/{id}/event-choices — upsert one row.
|
|
func handlePutProjectEventChoice(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if dbSvc.eventChoice == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "event-choice service not configured"})
|
|
return
|
|
}
|
|
projectID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
|
|
return
|
|
}
|
|
var input services.UpsertEventChoiceInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
|
|
return
|
|
}
|
|
row, err := dbSvc.eventChoice.Upsert(r.Context(), uid, projectID, input)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrInvalidInput) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, row)
|
|
}
|
|
|
|
// DELETE /api/projects/{id}/event-choices/{submission_code}/{choice_kind}
|
|
func handleDeleteProjectEventChoice(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if dbSvc.eventChoice == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "event-choice service not configured"})
|
|
return
|
|
}
|
|
projectID, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid project id"})
|
|
return
|
|
}
|
|
submissionCode := r.PathValue("submission_code")
|
|
choiceKind := r.PathValue("choice_kind")
|
|
if err := dbSvc.eventChoice.Delete(r.Context(), uid, projectID, submissionCode, choiceKind); err != nil {
|
|
if errors.Is(err, services.ErrInvalidInput) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|