package handlers import ( "encoding/json" "errors" "net/http" "github.com/google/uuid" "mgit.msbls.de/m/paliad/internal/services" ) // t-paliad-340 / m/paliad#153 B0 — REST endpoints over the new normalised // scenario builder shape (paliad.scenarios with owner_id, + // paliad.scenario_proceedings / scenario_events / scenario_shares). // // Endpoints live under /api/builder/scenarios/* to avoid clashing with // the legacy /api/scenarios/* endpoints from m/paliad#124 Slice D. The // B6 cleanup slice retires the legacy surface; until then both shapes // coexist on the same paliad.scenarios table (the legacy paths require // project_id IS NOT NULL OR an abstract created_by = caller; the builder // paths require owner_id = caller). // // All handlers gate by requireScenarioBuilderService — 503 when the // service is nil (DATABASE_URL unset). Auth is checked via requireUser; // per-row visibility is enforced inside the service. func requireScenarioBuilderService(w http.ResponseWriter) bool { if dbSvc == nil || dbSvc.scenarioBuilder == nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{ "error": "Litigation-Builder ist vorübergehend nicht verfügbar (keine Datenbank).", }) return false } return true } // scenarioBuilderErrorToStatus maps service errors to HTTP statuses. func scenarioBuilderErrorToStatus(err error) (int, string) { switch { case errors.Is(err, services.ErrScenarioBuilderNotVisible), errors.Is(err, services.ErrNotVisible): return http.StatusNotFound, "Szenario nicht gefunden" case errors.Is(err, services.ErrInvalidInput): return http.StatusBadRequest, err.Error() } return http.StatusInternalServerError, err.Error() } func writeBuilderError(w http.ResponseWriter, err error) { status, msg := scenarioBuilderErrorToStatus(err) writeJSON(w, status, map[string]string{"error": msg}) } // --------------------------------------------------------------------------- // Scenario CRUD // --------------------------------------------------------------------------- // handleBuilderScenariosList — GET /api/builder/scenarios?status= func handleBuilderScenariosList(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } status := r.URL.Query().Get("status") out, err := dbSvc.scenarioBuilder.ListMyScenarios(r.Context(), uid, status) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusOK, out) } // handleBuilderScenarioCreate — POST /api/builder/scenarios func handleBuilderScenarioCreate(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } var input services.CreateBuilderScenarioInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.CreateScenario(r.Context(), uid, input) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusCreated, out) } // handleBuilderScenarioGet — GET /api/builder/scenarios/{id} func handleBuilderScenarioGet(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } id, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige ID"}) return } out, err := dbSvc.scenarioBuilder.GetScenarioDeep(r.Context(), uid, id) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusOK, out) } // handleBuilderScenarioPatch — PATCH /api/builder/scenarios/{id} func handleBuilderScenarioPatch(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } id, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige ID"}) return } var input services.PatchBuilderScenarioInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.PatchScenario(r.Context(), uid, id, input) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusOK, out) } // --------------------------------------------------------------------------- // Proceedings // --------------------------------------------------------------------------- // handleBuilderProceedingCreate — POST /api/builder/scenarios/{id}/proceedings func handleBuilderProceedingCreate(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } var input services.AddProceedingInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.AddProceeding(r.Context(), uid, sid, input) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusCreated, out) } // handleBuilderProceedingPatch — PATCH /api/builder/scenarios/{id}/proceedings/{pid} func handleBuilderProceedingPatch(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } pid, err := uuid.Parse(r.PathValue("pid")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Proceeding-ID"}) return } var input services.PatchProceedingInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.PatchProceeding(r.Context(), uid, sid, pid, input) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusOK, out) } // handleBuilderProceedingDelete — DELETE /api/builder/scenarios/{id}/proceedings/{pid} func handleBuilderProceedingDelete(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } pid, err := uuid.Parse(r.PathValue("pid")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Proceeding-ID"}) return } if err := dbSvc.scenarioBuilder.DeleteProceeding(r.Context(), uid, sid, pid); err != nil { writeBuilderError(w, err) return } w.WriteHeader(http.StatusNoContent) } // --------------------------------------------------------------------------- // Events // --------------------------------------------------------------------------- // handleBuilderEventCreate — POST /api/builder/scenarios/{id}/proceedings/{pid}/events func handleBuilderEventCreate(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } pid, err := uuid.Parse(r.PathValue("pid")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Proceeding-ID"}) return } var input services.AddEventInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.AddEvent(r.Context(), uid, sid, pid, input) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusCreated, out) } // handleBuilderEventPatch — PATCH /api/builder/scenarios/{id}/events/{eid} func handleBuilderEventPatch(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } eid, err := uuid.Parse(r.PathValue("eid")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Event-ID"}) return } var input services.PatchEventInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.PatchEvent(r.Context(), uid, sid, eid, input) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusOK, out) } // handleBuilderEventDelete — DELETE /api/builder/scenarios/{id}/events/{eid} func handleBuilderEventDelete(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } eid, err := uuid.Parse(r.PathValue("eid")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Event-ID"}) return } if err := dbSvc.scenarioBuilder.DeleteEvent(r.Context(), uid, sid, eid); err != nil { writeBuilderError(w, err) return } w.WriteHeader(http.StatusNoContent) } // --------------------------------------------------------------------------- // Shares // --------------------------------------------------------------------------- // handleBuilderShareCreate — POST /api/builder/scenarios/{id}/shares // Body: {"shared_with_user_id": ""} func handleBuilderShareCreate(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } sid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } var body struct { SharedWithUserID uuid.UUID `json:"shared_with_user_id"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Anfrage"}) return } out, err := dbSvc.scenarioBuilder.AddShare(r.Context(), uid, sid, body.SharedWithUserID) if err != nil { writeBuilderError(w, err) return } writeJSON(w, http.StatusCreated, out) } // handleBuilderShareDelete — DELETE /api/builder/scenarios/{id}/shares/{sid} func handleBuilderShareDelete(w http.ResponseWriter, r *http.Request) { if !requireScenarioBuilderService(w) { return } uid, ok := requireUser(w, r) if !ok { return } scid, err := uuid.Parse(r.PathValue("id")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Szenario-ID"}) return } shid, err := uuid.Parse(r.PathValue("sid")) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "ungültige Share-ID"}) return } if err := dbSvc.scenarioBuilder.DeleteShare(r.Context(), uid, scid, shid); err != nil { writeBuilderError(w, err) return } w.WriteHeader(http.StatusNoContent) } // --------------------------------------------------------------------------- // Dev-only test route // --------------------------------------------------------------------------- // handleBuilderDevTestPage — GET /dev/scenario-builder // // Gated to services.PaliadinOwnerEmail (the same single-owner gate the // /paliadin route uses). Every other authenticated user gets 404. Pure // HTML — no JS bundle — so the page works even before B1 wires the real // builder shell. Renders curl-equivalent forms for the B0 surface so the // schema can be exercised end-to-end without Postman / shell scripts. // // This is the "dev-only test route" the head's task spec asked for. It // disappears in B6 cleanup once the production builder UI ships at // /tools/procedures. func handleBuilderDevTestPage(w http.ResponseWriter, r *http.Request) { if !requirePaliadinOwner(w, r) { return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = w.Write([]byte(builderDevTestHTML)) } const builderDevTestHTML = ` Scenario Builder — Dev Test (B0)

Scenario Builder — Dev Test (B0)

t-paliad-340 / m/paliad#153 — DB-only slice. Exercises paliad.scenarios (builder rows), scenario_proceedings, scenario_events, scenario_shares via /api/builder/scenarios/*. Gated to PaliadinOwnerEmail.

1. Liste meine Szenarien


2. Szenario anlegen


3. Szenario abrufen (deep)


4. Verfahren hinzufügen


5. Event-Karte hinzufügen


6. Status patchen (archive / restore)


`