{ "openapi": "3.1.0", "info": { "title": "Holzleitner Backend API", "description": "Backend f\u00fcr die Holzleitner-Lieferservice-App \u2014 Tour, Beladung, Ausf\u00fchrung.", "contact": { "name": "Holzleitner GmbH" }, "license": { "name": "Proprietary", "identifier": "Proprietary" }, "version": "0.1.0" }, "paths": { "/accounts/{personalnummer}": { "get": { "tags": [ "accounts" ], "summary": "Liest den Account zu einer Personalnummer.", "operationId": "get_account", "parameters": [ { "name": "personalnummer", "in": "path", "description": "Personalnummer des Accounts", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "200": { "description": "Account gefunden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Account" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Kein Account zu dieser Personalnummer" } }, "security": [ { "bearer_auth": [] } ] } }, "/admin/belege/{belegnummer}": { "get": { "tags": [ "admin" ], "summary": "Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung\nmit Positionen (inkl. Scan-St\u00e4nden), Kunde + Ansprechpartner, referenzierte\nArtikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte \u2014\ndestilliert aus dem Tour-Aggregat. Jede Lieferung, unabh\u00e4ngig vom Status.\n`404`, wenn die Belegnummer unbekannt ist.", "operationId": "belege_details", "parameters": [ { "name": "belegnummer", "in": "path", "description": "ERP-Belegnummer, z. B. V-30690291", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Volle Lieferdetails", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryDetails" } } } }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" }, "404": { "description": "Belegnummer unbekannt" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/belege/{belegnummer}/positions-modified": { "get": { "tags": [ "admin" ], "summary": "Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen\nver\u00e4ndert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) \u2014\nunabh\u00e4ngig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt\nist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag\n`true`, sobald **eine** davon ver\u00e4ndert ist.", "operationId": "positions_modified", "parameters": [ { "name": "belegnummer", "in": "path", "description": "ERP-Belegnummer, z. B. V-30690291", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "\u00c4nderungs-Flag der Belegpositionen", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PositionsModifiedResponse" } } } }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" }, "404": { "description": "Belegnummer unbekannt" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/completed-deliveries": { "get": { "tags": [ "admin" ], "summary": "Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen \u2014 optional auf\neinen Datumsbereich eingegrenzt, unabh\u00e4ngig vom Mail-Versand-Status.\nGefiltert wird \u00fcber den **Berliner** Kalendertag des Abschluss-Zeitpunkts\n(`completed_at`).", "description": "Parameter (alle `DD-MM-YYYY`, alle optional): `day` = Einzeltag (Kurzform\n`from=to=day`), sonst `from`/`to` als **inklusive** Bereichsgrenzen (je\noffen). **Ohne jeden Parameter \u2192 ALLE ausgelieferten Belege** (kein\nDatumsfilter). Pro Lieferung: Belegnummer + `positions_modified` (Menge\nreduziert/Zeile entfernt oder Geld-Gutschrift). Die Halb-Grenzen-Variante ist\nf\u00fcr Range-Filter gedacht: `?from=\u2026` und `?to=\u2026` liefern je eine Menge, deren\nSQL-`AND`-Schnitt den Zeitraum ergibt.", "operationId": "completed_deliveries", "parameters": [ { "name": "day", "in": "query", "description": "Einzeltag DD-MM-YYYY (Kurzform from=to)", "required": false, "schema": { "type": "string" } }, { "name": "from", "in": "query", "description": "Untere Grenze DD-MM-YYYY (inklusive)", "required": false, "schema": { "type": "string" } }, { "name": "to", "in": "query", "description": "Obere Grenze DD-MM-YYYY (inklusive)", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Ausgelieferte Lieferungen (optional bereichsgefiltert)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompletedDeliveriesResponse" } } } }, "400": { "description": "Ung\u00fcltiges Datum" }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/delivered-belegnummern": { "get": { "tags": [ "admin" ], "summary": "Liefert die Belegnummern ausgelieferter (abgeschlossener) Lieferungen,\n**deren Liefermail noch nicht versendet wurde** (`mail_sent_at IS NULL`).\n\u201eAusgeliefert\" = es existiert ein Abschluss. Mit `day` (DD-MM-YYYY) nur\nAbschl\u00fcsse dieses Berliner Kalendertages; **ohne `day` alle offenen** (\u00fcber\nalle Tage) \u2014 so bleiben Belege \u00fcber Mitternacht nicht h\u00e4ngen.", "operationId": "delivered_belegnummern", "parameters": [ { "name": "day", "in": "query", "description": "Tag DD-MM-YYYY; ohne Angabe ALLE offenen Belege", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Offene (nicht versendete) Belegnummern", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveredBelegnummernResponse" } } } }, "400": { "description": "Ung\u00fcltiger Tag" }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/import-erp": { "post": { "tags": [ "admin" ], "summary": "St\u00f6\u00dft den ERP-Import f\u00fcr ein Datum an und liefert die Zusammenfassung.", "operationId": "import_erp", "parameters": [ { "name": "date", "in": "query", "description": "Ziel-Tourdatum YYYY-MM-DD (Default: heute)", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Import durchgef\u00fchrt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImportSummary" } } } }, "400": { "description": "Ung\u00fcltiges Datum" }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" }, "502": { "description": "ERP nicht erreichbar / Lesefehler" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/mark-mail-sent": { "post": { "tags": [ "admin" ], "summary": "Markiert die Liefermails der angegebenen Belegnummern als **versendet**\n(`mail_sent_at = now()`, nur wo noch offen). Vom Mailclient aufzurufen,\nNACHDEM ERPframe die Mails erfolgreich verschickt hat \u2014 danach erscheinen\ndie Belege nicht mehr in `GET /admin/delivered-belegnummern`.", "operationId": "mark_mail_sent", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MarkMailSentRequest" } } }, "required": true }, "responses": { "200": { "description": "Markierung durchgef\u00fchrt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MarkMailSentResponse" } } } }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/push-completion": { "post": { "tags": [ "admin" ], "summary": "St\u00f6\u00dft das ERP-R\u00fcckschreiben eines bereits lokal abgeschlossenen\nLieferabschlusses erneut an (idempotenter Retry, falls der automatische\nPush beim Abschluss fehlschlug).", "operationId": "push_completion", "parameters": [ { "name": "delivery_id", "in": "query", "description": "UUID der abgeschlossenen Lieferung", "required": true, "schema": { "type": "string" } } ], "responses": { "204": { "description": "R\u00fcckschreiben erfolgreich" }, "400": { "description": "Ung\u00fcltige delivery_id" }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" }, "404": { "description": "Lieferung nicht gefunden / nicht abgeschlossen" }, "502": { "description": "ERP nicht erreichbar / Schreibfehler" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/reviews": { "get": { "tags": [ "admin" ], "summary": "Listet alle ge\u00e4nderten Lieferscheine, die noch auf eine manuelle\nBest\u00e4tigung (Vier-Augen) warten \u2014 Entfernungen und/oder Geld-Gutschriften.", "operationId": "list_reviews", "responses": { "200": { "description": "Offene Pr\u00fcfungen", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PendingReviewResponse" } } } } }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" } }, "security": [ { "admin_api_key": [] } ] } }, "/admin/reviews/{delivery_id}/resolve": { "post": { "tags": [ "admin" ], "summary": "Best\u00e4tigt die Pr\u00fcfung einer ge\u00e4nderten Lieferung (Vier-Augen) \u2014 die\nLieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht\nerneut ge\u00e4ndert).", "operationId": "resolve_review", "parameters": [ { "name": "delivery_id", "in": "path", "description": "UUID der Lieferung", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResolveReviewRequest" } } }, "required": true }, "responses": { "204": { "description": "Pr\u00fcfung best\u00e4tigt" }, "400": { "description": "Ung\u00fcltige delivery_id / Bearbeiter leer" }, "401": { "description": "Admin-API-Key fehlt/ung\u00fcltig" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "admin_api_key": [] } ] } }, "/attachments/{id}": { "get": { "tags": [ "attachments" ], "summary": "Liefert ein gerendertes Vorschaubild des Attachments (Bytes), geladen\naus DOCUframe. Aufl\u00f6sung/Format \u00fcber Query-Parameter steuerbar\n(`?w=&h=&q=&ext=&page=`).", "operationId": "get_attachment", "parameters": [ { "name": "id", "in": "path", "description": "Attachment-Id (unsere UUID)", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "w", "in": "query", "description": "Breite in Pixeln (Default 1024)", "required": false, "schema": { "type": "integer", "format": "int32", "minimum": 0 } }, { "name": "h", "in": "query", "description": "H\u00f6he in Pixeln (Default 1024)", "required": false, "schema": { "type": "integer", "format": "int32", "minimum": 0 } }, { "name": "q", "in": "query", "description": "Qualit\u00e4t 0\u2013100 (Default 85)", "required": false, "schema": { "type": "integer", "format": "int32", "minimum": 0 } }, { "name": "ext", "in": "query", "description": "png|jpeg|jpg|webp|tiff (Default jpeg)", "required": false, "schema": { "type": "string" } }, { "name": "page", "in": "query", "description": "Seitennummer (Default 1)", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Vorschaubild (Bytes)", "content": { "image/jpeg": {} } }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Attachment nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/assigned-car": { "put": { "tags": [ "deliveries" ], "summary": "Setzt das `assigned_car_id` einer Lieferung. `carId: null` l\u00f6st\ndie Zuordnung wieder. Der Use Case stellt sicher, dass das Fahrzeug\nzum angemeldeten Account geh\u00f6rt.", "operationId": "assign_car", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AssignCarRequest" } } }, "required": true }, "responses": { "200": { "description": "Fahrzeug zugewiesen / entfernt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryResponse" } } } }, "400": { "description": "Fahrzeug geh\u00f6rt nicht zum Account" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/cancel": { "post": { "tags": [ "deliveries" ], "summary": "Setzt die Lieferung auf `canceled` \u2014 endg\u00fcltig. Erlaubt aus\n`active` und `held`.", "operationId": "cancel", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelDeliveryRequest" } } }, "required": true }, "responses": { "200": { "description": "Lieferung storniert", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryResponse" } } } }, "400": { "description": "Invalider Status\u00fcbergang oder leerer Reason" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/complete": { "post": { "tags": [ "deliveries" ], "summary": "Schlie\u00dft die Lieferung ab \u2014 `state = completed`. Nur aus `active`.", "description": "`multipart/form-data` mit drei Feldern:\n * `customer_signature` \u2014 PNG der Kunden-Unterschrift (Pflicht)\n * `driver_signature` \u2014 PNG der Fahrer-Unterschrift (Pflicht)\n * `acknowledgements` \u2014 JSON (`CompleteDeliveryAcknowledgements`):\n `receiptConfirmed` (Pflicht true), `notesAcknowledged`,\n `acknowledgedNoteIds`, `authorCarId`.\n\nAtomar: Signaturen werden lokal gespeichert, die Abschluss-Zeile\ngeschrieben und der Status auf `completed` gesetzt \u2014 alles oder nichts.\nGates: Lieferung aktiv, alle scanbaren Positionen fertig, Notizen\nbest\u00e4tigt (falls vorhanden).", "operationId": "complete", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Felder `customer_signature`, `driver_signature` (PNG) + `acknowledgements` (JSON)", "content": { "multipart/form-data": {} } }, "responses": { "200": { "description": "Lieferung abgeschlossen", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryResponse" } } } }, "400": { "description": "Invalider Status\u00fcbergang / fehlende Signatur / offene Scans / Notizen unbest\u00e4tigt" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/credit": { "post": { "tags": [ "deliveries" ], "summary": "Wendet ein Betrags-Gutschrift-Ereignis an (`set`/`remove`). Append-only,\nidempotent \u00fcber `clientEventId`. Nur bei aktiver Lieferung; bei `set` sind\nBetrag (0 < x \u2264 150 \u20ac, 10-\u20ac-Schritte) und Grund Pflicht. Antwort: der\naktuelle Gutschrift-Stand (`null`, wenn entfernt).", "operationId": "apply_credit", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryCreditEventRequest" } } }, "required": true }, "responses": { "200": { "description": "Gutschrift gesetzt/entfernt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryCreditResponse" } } } }, "400": { "description": "Ung\u00fcltiger Betrag/Grund oder Lieferung nicht aktiv" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/hold": { "post": { "tags": [ "deliveries" ], "summary": "Setzt die Lieferung auf `held`. Nur aus `active` zul\u00e4ssig.", "operationId": "hold", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HoldDeliveryRequest" } } }, "required": true }, "responses": { "200": { "description": "Lieferung geholdet", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryResponse" } } } }, "400": { "description": "Invalider Status\u00fcbergang oder leerer Reason" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/notes": { "post": { "tags": [ "deliveries" ], "summary": "Legt eine neue Notiz an einer Lieferung an. Mindestens eines von\n`text` und `imageAttachment` muss inhaltlich gef\u00fcllt sein\n(Leerstrings werden serverseitig getrimmt und als leer behandelt).", "operationId": "create_note", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateDeliveryNoteRequest" } } }, "required": true }, "responses": { "200": { "description": "Notiz angelegt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryNoteResponse" } } } }, "400": { "description": "Notiz ohne Inhalt" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/notes/image": { "post": { "tags": [ "deliveries" ], "summary": "L\u00e4dt ein Bild zu einer Lieferung hoch (multipart/form-data, Feld `file`)\nund legt daf\u00fcr eine Bild-Notiz an. Das Bild geht in den\nDOCUframe-Dokumentenspeicher; gespeichert wird die zur\u00fcckgelieferte\nReferenz (`~ObjectID`) als `image_attachment` der Notiz.", "operationId": "upload_note_image", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "description": "Formularfeld `file` mit den Bilddaten", "content": { "multipart/form-data": {} } }, "responses": { "200": { "description": "Bild hochgeladen, Notiz angelegt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryNoteResponse" } } } }, "400": { "description": "Kein/leeres Datei-Feld" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" }, "500": { "description": "Upload zu DOCUframe fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/notes/{note_id}": { "delete": { "tags": [ "deliveries" ], "summary": "L\u00f6scht eine Notiz. Antwortet mit `204 No Content`.", "operationId": "delete_note", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "note_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Notiz gel\u00f6scht" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Notiz nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] }, "patch": { "tags": [ "deliveries" ], "summary": "\u00c4ndert Text/Bild einer Notiz. Innerhalb des (geteilten) Accounts darf\njeder Fahrer Notizen pflegen \u2014 kein Autor-Check. `delivery_id` ist Teil\ndes Pfads (REST-Konsistenz), die Notiz wird \u00fcber `note_id` adressiert.", "operationId": "update_note", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "note_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateDeliveryNoteRequest" } } }, "required": true }, "responses": { "200": { "description": "Notiz aktualisiert", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryNoteResponse" } } } }, "400": { "description": "Notiz ohne Inhalt" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Notiz nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/payment": { "post": { "tags": [ "deliveries" ], "summary": "Protokolliert die Zahlungsabwicklung (\u201eAbkassieren\") einer Lieferung.\nAppend-only, idempotent \u00fcber `clientEventId`. Der Server berechnet den\noffenen Betrag selbst; er muss > 0 sein und `expectedAmountCents`\nentsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode.", "operationId": "record_payment", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordDeliveryPaymentRequest" } } }, "required": true }, "responses": { "200": { "description": "Zahlung protokolliert", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryPaymentResponse" } } } }, "400": { "description": "Lieferung nicht aktiv, Methode ung\u00fcltig, kein offener Betrag oder Betrag abweichend" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/resume": { "post": { "tags": [ "deliveries" ], "summary": "Setzt die Lieferung zur\u00fcck auf `active`. Nur aus `held` zul\u00e4ssig.", "operationId": "resume", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Lieferung wieder aktiv", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryResponse" } } } }, "400": { "description": "Invalider Status\u00fcbergang" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/deliveries/{delivery_id}/services/{service_id}": { "put": { "tags": [ "deliveries" ], "summary": "Setzt (Upsert) den Wert eines Service f\u00fcr eine Lieferung. Genau das zum\nService-Typ passende Feld (`boolValue`/`numericValue`) muss gesetzt sein;\nnumerische Werte werden gegen min/max gepr\u00fcft. Nur bei aktiver Lieferung.", "operationId": "set_service", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "service_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetDeliveryServiceRequest" } } }, "required": true }, "responses": { "200": { "description": "Wert gesetzt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeliveryServiceResponse" } } } }, "400": { "description": "Wert passt nicht zum Service-Typ / au\u00dferhalb min-max / Lieferung nicht aktiv" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Service oder Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] }, "delete": { "tags": [ "deliveries" ], "summary": "Entfernt den Service-Wert einer Lieferung (Service \u201enicht gesetzt\").\nNur bei aktiver Lieferung. Antwort `204`.", "operationId": "delete_service_value", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "service_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Wert entfernt" }, "400": { "description": "Lieferung nicht aktiv" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Lieferung nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/health": { "get": { "tags": [ "health" ], "summary": "Health-Endpoint f\u00fcr Load-Balancer und Container-Probes. Bewusst\nkein Auth \u2014 eine `200 ok`-Antwort darf nicht von der Auth abh\u00e4ngen.", "operationId": "health", "responses": { "200": { "description": "Service ist erreichbar", "content": { "text/plain": { "schema": { "type": "string" } } } } }, "security": [] } }, "/me/cars": { "get": { "tags": [ "cars" ], "summary": "Listet die Fahrzeuge des angemeldeten Fahrers.", "operationId": "list_my_cars", "parameters": [ { "name": "includeInactive", "in": "query", "description": "Wenn true, werden inaktive Fahrzeuge mitgeliefert (default: false)", "required": false, "schema": { "type": "boolean" } } ], "responses": { "200": { "description": "Fahrzeuge des Fahrers", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CarsList" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] }, "post": { "tags": [ "cars" ], "summary": "Legt ein neues Fahrzeug f\u00fcr den angemeldeten Fahrer an.", "operationId": "create_my_car", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateCarRequest" } } }, "required": true }, "responses": { "200": { "description": "Fahrzeug angelegt", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CarResponse" } } } }, "400": { "description": "Validierungsfehler (z. B. doppeltes Kennzeichen)" }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] } }, "/me/cars/{car_id}": { "patch": { "tags": [ "cars" ], "summary": "Aktualisiert ein Fahrzeug (Kennzeichen \u00e4ndern / deaktivieren).", "operationId": "update_my_car", "parameters": [ { "name": "car_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateCarRequest" } } }, "required": true }, "responses": { "200": { "description": "Fahrzeug aktualisiert", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CarResponse" } } } }, "400": { "description": "Validierungsfehler" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Fahrzeug nicht gefunden oder geh\u00f6rt nicht zu diesem Account" } }, "security": [ { "bearer_auth": [] } ] } }, "/me/tours": { "get": { "tags": [ "tours" ], "summary": "Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) f\u00fcr ein\nZiel-Datum. Ohne `?date=` gilt \u201eheute\". Der Fahrer w\u00e4hlt das Datum aktiv\nim Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen).", "operationId": "list_my_tours", "parameters": [ { "name": "date", "in": "query", "description": "Ziel-Tourdatum YYYY-MM-DD (Default: heute)", "required": false, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Liste der Touren am Ziel-Datum", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TourSummaryList" } } } }, "400": { "description": "Ung\u00fcltiges Datum" }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] } }, "/payment-methods": { "get": { "tags": [ "payment-methods" ], "summary": "Listet die Zahlungsmethoden.", "operationId": "list_payment_methods", "parameters": [ { "name": "includeInactive", "in": "query", "description": "Wenn true, werden inaktive Methoden mitgeliefert (default: false)", "required": false, "schema": { "type": "boolean" } } ], "responses": { "200": { "description": "Zahlungsmethoden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentMethodsList" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] }, "post": { "tags": [ "payment-methods" ], "summary": "Legt eine neue Zahlungsmethode an.", "operationId": "create_payment_method", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreatePaymentMethodRequest" } } }, "required": true }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentMethodResponse" } } } }, "400": { "description": "Validierungsfehler (z. B. doppelter code)" }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] } }, "/payment-methods/{id}": { "delete": { "tags": [ "payment-methods" ], "summary": "Hartes L\u00f6schen. `409 Conflict`, wenn die Methode von einer Lieferung\nreferenziert wird \u2014 der Admin soll dann den `active = false`-Pfad\nnutzen.", "operationId": "delete_payment_method", "parameters": [ { "name": "id", "in": "path", "description": "Zahlungsmethoden-Id", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Methode gel\u00f6scht" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Methode nicht gefunden" }, "409": { "description": "Methode ist noch von Lieferungen referenziert" } }, "security": [ { "bearer_auth": [] } ] }, "patch": { "tags": [ "payment-methods" ], "summary": "Patcht Anzeige-Name und/oder Aktiv-Flag.", "operationId": "update_payment_method", "parameters": [ { "name": "id", "in": "path", "description": "Zahlungsmethoden-Id", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdatePaymentMethodRequest" } } }, "required": true }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentMethodResponse" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Methode nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/scans": { "post": { "tags": [ "scans" ], "summary": "Wendet eine Liste von Scan-Events idempotent an.", "description": "Pro Event ein eigenes Resultat. Status `applied` schreibt einen\nfrischen Audit-Eintrag, `duplicate` liefert den aktuellen Stand am\nServer, `rejected` enth\u00e4lt die Begr\u00fcndung. Reihenfolge der `results`\nentspricht der Reihenfolge der `scans` im Request.", "operationId": "apply_scans", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApplyScansRequest" } } }, "required": true }, "responses": { "200": { "description": "Bulk-Ergebnis pro Event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApplyScansResponse" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] } }, "/services": { "get": { "tags": [ "services" ], "summary": "Listet die Services (sortiert nach `sortOrder`).", "operationId": "list_services", "parameters": [ { "name": "includeInactive", "in": "query", "description": "Wenn true, werden inaktive Services mitgeliefert (default: false)", "required": false, "schema": { "type": "boolean" } } ], "responses": { "200": { "description": "Services", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ServicesList" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] }, "post": { "tags": [ "services" ], "summary": "Legt einen neuen Service an.", "operationId": "create_service", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateServiceRequest" } } }, "required": true }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ServiceResponse" } } } }, "400": { "description": "Validierungsfehler (z. B. kind/min/max inkonsistent)" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "409": { "description": "key existiert bereits" } }, "security": [ { "bearer_auth": [] } ] } }, "/services/{id}": { "delete": { "tags": [ "services" ], "summary": "Hartes L\u00f6schen. `409 Conflict`, wenn der Service noch von einer Lieferung\nreferenziert wird \u2014 dann stattdessen deaktivieren.", "operationId": "delete_service", "parameters": [ { "name": "id", "in": "path", "description": "Service-Id", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "Service gel\u00f6scht" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Service nicht gefunden" }, "409": { "description": "Service ist noch referenziert" } }, "security": [ { "bearer_auth": [] } ] }, "patch": { "tags": [ "services" ], "summary": "Patcht Name/Grenzen/Aktiv-Flag/Sortierung. `kind` ist nicht \u00e4nderbar.", "operationId": "update_service", "parameters": [ { "name": "id", "in": "path", "description": "Service-Id", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateServiceRequest" } } }, "required": true }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ServiceResponse" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Service nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } }, "/sync/tour": { "post": { "tags": [ "sync" ], "summary": "Sync-Endpoint f\u00fcr das ERP: legt eine Tagestour samt Lieferungen und\nPositionen idempotent an. Identit\u00e4t pro Tour\n`(driver_personalnummer, tour_date)`, pro Lieferung\n`(belegart_id, belegnummer)`.", "operationId": "sync_tour", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SyncTourRequest" } } }, "required": true }, "responses": { "200": { "description": "Tour gespeichert", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SyncTourResponse" } } } }, "400": { "description": "Validierungsfehler im Sync-Payload" }, "401": { "description": "Authentifizierung fehlgeschlagen" } }, "security": [ { "bearer_auth": [] } ] } }, "/tours/{tour_id}": { "get": { "tags": [ "tours" ], "summary": "L\u00e4dt eine Tour mit allen Lieferungen, Positionen und referenzierten\nStammdaten \u2014 die App nutzt das als einzigen gro\u00dfen Read.", "operationId": "get_tour", "parameters": [ { "name": "tour_id", "in": "path", "description": "Eindeutige Tour-Id (UUID)", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Tour-Aggregat gefunden", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TourDetails" } } } }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Keine Tour mit dieser Id" } }, "security": [ { "bearer_auth": [] } ] } }, "/tours/{tour_id}/delivery-order": { "put": { "tags": [ "tours" ], "summary": "Schreibt die Sortier-Reihenfolge aller Lieferungen einer Tour neu.\nDer Client schickt die **vollst\u00e4ndige** neue Reihenfolge; fehlende\noder fremde Lieferungs-Ids werden mit `400 validation` abgelehnt.", "operationId": "set_delivery_order", "parameters": [ { "name": "tour_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetDeliveryOrderRequest" } } }, "required": true }, "responses": { "200": { "description": "Neue Reihenfolge gespeichert", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SetDeliveryOrderResponse" } } } }, "400": { "description": "Mengen-Mismatch oder Duplikate" }, "401": { "description": "Authentifizierung fehlgeschlagen" }, "404": { "description": "Tour nicht gefunden" } }, "security": [ { "bearer_auth": [] } ] } } }, "components": { "schemas": { "Account": { "type": "object", "description": "Account eines Liefer-Unternehmens oder Einzel-Lieferfahrers.\n\nDie Personalnummer ist sowohl Prim\u00e4rschl\u00fcssel als auch Login-ID. Sie\nstammt aus dem ERP-Stamm \u2014 entweder ein Unternehmen (juristische\nPerson, eigener Personalnummern-Kreis) oder eine nat\u00fcrliche Person.\n\nMehrere physische Fahrer k\u00f6nnen denselben Account benutzen; das Modell\nunterscheidet sie nicht, sondern loggt die Aktivit\u00e4t auf [`crate::domain::Car`]-\nEbene (siehe Audit-Log).", "required": [ "personalnummer", "name", "active" ], "properties": { "active": { "type": "boolean" }, "name": { "type": "string" }, "personalnummer": { "type": "integer", "format": "int64" } } }, "Address": { "type": "object", "description": "Postanschrift \u2014 wird sowohl als aktuelle Kundenanschrift in [`Customer`]\nals auch als unver\u00e4nderlicher Snapshot in [`crate::domain::Delivery`]\nverwendet (`delivery_address_snapshot`).\n\nBewusst als Value Object modelliert: gleiche Adresse = gleicher Wert.\nStrikte Equality erleichtert Sync-Diffs zwischen ERP und Backend.\n\n[`Customer`]: crate::domain::Customer", "required": [ "street", "houseNumber", "postalCode", "city", "country" ], "properties": { "addition": { "type": [ "string", "null" ], "description": "Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das\ndie Sachbearbeitung praktisch f\u00fcr Ortsteil, Etage oder Hinweise nutzt\n(z. B. \u201eHolzheim\", \u201e3. Etage\", \u201eParterre\"). `None`, wenn leer." }, "city": { "type": "string" }, "country": { "type": "string", "description": "Land laut ERP-L\u00e4nderstamm (`Adressen.LandID` \u2192 `Laender.Land`), z. B.\n\u201eDeutschland\" oder \u201eBelgien\". Leer, wenn im ERP kein Land verkn\u00fcpft ist." }, "houseNumber": { "type": "string" }, "postalCode": { "type": "string" }, "street": { "type": "string" } } }, "ApplyScansRequest": { "type": "object", "required": [ "scans" ], "properties": { "scans": { "type": "array", "items": { "$ref": "#/components/schemas/ScanEvent" } } } }, "ApplyScansResponse": { "type": "object", "required": [ "results" ], "properties": { "results": { "type": "array", "items": { "$ref": "#/components/schemas/ScanResult" } } } }, "Article": { "type": "object", "description": "Artikel. ERP-Mirror; die `article_number` ist die business-stabile\nArtikelnummer aus dem ERP-Stamm und dient gleichzeitig als Br\u00fccke.\n\n`scannable = false` markiert nicht-physische Positionen wie\nDienstleistungen, Versandpauschalen o.\u00e4. \u2014 sie tauchen zwar als\n`DeliveryItem` auf, blockieren aber den Beladen-Fortschritt nicht.", "required": [ "id", "articleNumber", "name", "scannable" ], "properties": { "articleNumber": { "type": "string" }, "defaultWarehouseId": { "type": [ "string", "null" ], "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "scannable": { "type": "boolean" } } }, "AssignCarRequest": { "type": "object", "description": "Setzt das `assigned_car_id` einer Lieferung. `None` (`carId: null`)\nentfernt die Zuordnung.", "properties": { "carId": { "type": [ "string", "null" ], "format": "uuid" } } }, "AuditAction": { "type": "string", "description": "Aktion-Typen im Scan-Audit-Log.\n\n* `Scan` / `Unscan` ver\u00e4ndern die `scanned_quantity` (+1 / -1).\n* `Hold` / `Unhold` \u00e4ndern nur den Status, keine Menge.\n* `Remove` markiert die Position als entfernt (Status `Removed`,\n z. B. weil der Kunde sie nicht annimmt).\n* `Unremove` hebt ein `Remove` wieder auf \u2014 die Position landet\n zur\u00fcck in `InProgress` (oder `Done`, falls die `scanned_quantity`\n schon `required_quantity` erreicht hatte). Der urspr\u00fcngliche\n `Remove`-Audit-Eintrag bleibt unangetastet; das `Unremove` erzeugt\n einen eigenen Audit-Eintrag \u2014 die Historie bleibt vollst\u00e4ndig.", "enum": [ "scan", "unscan", "hold", "unhold", "remove", "unremove" ] }, "CancelDeliveryRequest": { "type": "object", "required": [ "reason" ], "properties": { "reason": { "type": "string" } } }, "Car": { "type": "object", "description": "Fahrzeug eines [`crate::domain::Account`]. Wird in der App selbst\ngepflegt \u2014 kein ERP-Spiegel. Eindeutig per UUID.\n\nIm Audit-Log ist der `Car` der \u201eAkteur\": die Personalnummer-Ebene\n(Account) ist gr\u00f6ber und unterscheidet nicht zwischen mehreren\ngleichzeitig aktiven Fahrern desselben Subunternehmens.", "required": [ "id", "accountId", "plate", "active" ], "properties": { "accountId": { "type": "integer", "format": "int64", "description": "Verweis auf [`crate::domain::Account::personalnummer`]." }, "active": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "plate": { "type": "string" } } }, "CarResponse": { "type": "object", "required": [ "car" ], "properties": { "car": { "$ref": "#/components/schemas/Car" } } }, "CarsList": { "type": "object", "required": [ "cars" ], "properties": { "cars": { "type": "array", "items": { "$ref": "#/components/schemas/Car" } } } }, "CompleteDeliveryAcknowledgements": { "type": "object", "description": "Dokumentierte Best\u00e4tigungen des Kunden zum Abschlusszeitpunkt.", "required": [ "receiptConfirmed" ], "properties": { "acknowledgedNoteIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Notiz-IDs, die zum Abschlusszeitpunkt sichtbar waren und mit-best\u00e4tigt\nwurden (Audit-Robustheit)." }, "authorCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug des Akteurs (Audit-Spur). Muss zum Account geh\u00f6ren." }, "internalNote": { "type": [ "string", "null" ], "description": "Optionale interne Notiz des Fahrers, abgefragt nach beiden\nUnterschriften. Nur f\u00fcr interne Zwecke (Lieferbericht), nicht Teil\nder Kunden-Best\u00e4tigung. Leer/Whitespace = keine Notiz; max. 2000 Zeichen." }, "notesAcknowledged": { "type": "boolean", "description": "\u201eAnmerkungen zur Lieferung zur Kenntnis genommen.\" \u2014 Pflicht nur, wenn\nNotizen existieren (das pr\u00fcft der Server)." }, "paymentCollected": { "type": "boolean", "description": "Inkasso-Best\u00e4tigung des Fahrers: \u201eder offene Betrag wurde erhalten\n(bar) bzw. \u00fcber das EC-Ger\u00e4t abgerechnet.\" Pflicht nur, wenn beim\nAbschluss ein offener Betrag > 0 besteht UND die Methode ein Vor-Ort-\nInkasso ist (Bar/EC) \u2014 das pr\u00fcft der Server. Der kassierte Betrag wird\nserver-seitig autoritativ berechnet (nicht vom Client \u00fcbernommen)." }, "paymentMethodId": { "type": [ "string", "null" ], "format": "uuid", "description": "Optionale Zahlungsmethode, die der Fahrer beim Abschluss gew\u00e4hlt hat.\n`None` = die am Beleg hinterlegte Methode bleibt. Falls gesetzt, muss\nsie existieren **und** aktiv sein (vom Server gepr\u00fcft)." }, "receiptConfirmed": { "type": "boolean", "description": "\u201eWare im ordnungsgem\u00e4\u00dfen Zustand erhalten / Aufbau korrekt.\" \u2014 Pflicht." } } }, "CompletedDeliveriesResponse": { "type": "object", "required": [ "count", "deliveries" ], "properties": { "count": { "type": "integer", "description": "Anzahl der abgeschlossenen Lieferungen im Bereich.", "minimum": 0 }, "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/CompletedDeliveryItem" }, "description": "Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt." }, "from": { "type": [ "string", "null" ], "description": "Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen)." }, "to": { "type": [ "string", "null" ], "description": "Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen)." } } }, "CompletedDeliveryItem": { "type": "object", "description": "Eine abgeschlossene Lieferung im Tagesabruf.", "required": [ "belegnummer", "positions_modified" ], "properties": { "belegnummer": { "type": "string", "description": "ERP-Belegnummer der Lieferung." }, "positions_modified": { "type": "boolean", "description": "`true`, wenn an der Lieferung Positionen ver\u00e4ndert wurden \u2014 eine Zeile\nwurde entfernt oder in der Menge reduziert (St\u00fcck-Gutschrift) **oder** es\nliegt eine aktive Geld-Gutschrift vor." } } }, "ContactChannel": { "type": "object", "description": "Ein einzelner Kontaktkanal (Telefonnummer / Mobil / E-Mail / Web).\nMehrere pro [`ContactSource`] m\u00f6glich, die `position` h\u00e4lt die\n1-basierte ERP-Reihenfolge (`Telefon` \u2192 1, `Telefon2` \u2192 2 usw.) fest,\ndamit der \u201eprim\u00e4re\" Kanal je Art stabil identifizierbar bleibt.", "required": [ "id", "sourceId", "kind", "position", "value" ], "properties": { "id": { "type": "string", "format": "uuid" }, "kind": { "$ref": "#/components/schemas/ContactKind" }, "position": { "type": "integer", "format": "int32" }, "sourceId": { "type": "string", "format": "uuid" }, "value": { "type": "string" } } }, "ContactKind": { "type": "string", "description": "Art eines Kommunikationskanals. `fax` bewusst nicht mitgef\u00fchrt \u2014 in der\nApp nicht verwendet.", "enum": [ "phone", "mobile", "email", "web" ] }, "ContactRole": { "type": "string", "description": "Rolle, mit der ein Kontakt-Datensatz an einer Lieferung h\u00e4ngt. Spiegelt\ndie f\u00fcnf Adress-FKs von `Belegkopf` (bzw. den Umweg \u00fcber den Kunden):\n`header` = Belegadresse, `delivery` = Lieferadresse, `billing` =\nRechnungsadresse, `contact_person` = Ansprechpartner, `customer_master`\n= Stammadresse des Kunden \u00fcber `Kunden.AdressId`.", "enum": [ "header", "delivery", "billing", "contact_person", "customer_master" ] }, "ContactSource": { "type": "object", "description": "Snapshot eines ERP-Adress-Datensatzes, der zum Zeitpunkt des Tour-Syncs\nan einer Lieferung hing \u2014 Namensblock ohne Anschrift, weil die Adresse\nihrerseits schon im Lieferungs-Snapshot steckt (`snap_*`-Spalten). Die\neigentlichen Telefonnummern, E-Mails etc. liegen in den\nzugeh\u00f6rigen [`ContactChannel`]s.", "required": [ "id", "deliveryId", "role" ], "properties": { "abteilung": { "type": [ "string", "null" ] }, "anrede": { "type": [ "string", "null" ] }, "deliveryId": { "type": "string", "format": "uuid" }, "funktion": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "name1": { "type": [ "string", "null" ] }, "name2": { "type": [ "string", "null" ] }, "name3": { "type": [ "string", "null" ] }, "role": { "$ref": "#/components/schemas/ContactRole" }, "titel": { "type": [ "string", "null" ] } } }, "CreateCarRequest": { "type": "object", "required": [ "plate" ], "properties": { "plate": { "type": "string" } } }, "CreateDeliveryNoteRequest": { "type": "object", "properties": { "authorCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug, das die Notiz erzeugt hat. Muss zum angemeldeten\nAccount geh\u00f6ren. `None` ist erlaubt." }, "creditDeliveryItemId": { "type": [ "string", "null" ], "format": "uuid", "description": "Optionaler Gutschrift-Bezug: die Belegzeile, f\u00fcr die diese Notiz als\nGutschrift-Grund angelegt wird. Erm\u00f6glicht das gezielte L\u00f6schen beim\nUnremove. `None` f\u00fcr normale Notizen." }, "imageAttachment": { "type": [ "string", "null" ], "description": "Object-Storage-Key oder URL eines vorab hochgeladenen Bildes." }, "isAmountCreditNote": { "type": "boolean", "description": "`true` markiert die Notiz als Grund einer Betrags-Gutschrift\n(Lieferungs-Ebene). Default `false`." }, "text": { "type": [ "string", "null" ] } } }, "CreatePaymentMethodRequest": { "type": "object", "required": [ "code", "name" ], "properties": { "code": { "type": "string", "description": "Eindeutiger Programm-Identifier (z. B. `\"paypal\"`, `\"klarna\"`)." }, "name": { "type": "string", "description": "Anzeige-Name in der UI." } } }, "CreateServiceRequest": { "type": "object", "required": [ "key", "name", "kind" ], "properties": { "key": { "type": "string", "description": "Eindeutiger Programm-Identifier (z. B. `\"podium_setup\"`)." }, "kind": { "$ref": "#/components/schemas/ServiceKind" }, "maxValue": { "type": [ "integer", "null" ], "format": "int32" }, "minValue": { "type": [ "integer", "null" ], "format": "int32", "description": "Nur bei `Numeric` sinnvoll." }, "name": { "type": "string" }, "sortOrder": { "type": [ "integer", "null" ], "format": "int32" } } }, "CreditAction": { "type": "string", "description": "Art des Gutschrift-Ereignisses.", "enum": [ "set", "remove" ] }, "Customer": { "type": "object", "description": "Kunde. ERP-Mirror: die Stammdaten geh\u00f6ren dem ERP, wir spiegeln sie\nf\u00fcr die App. Die `erp_customer_id` ist die Br\u00fccke zur\u00fcck (in der\nRegel die `Kunde.row_id` aus ERPframe).\n\nDie `Customer.address` ist die *aktuelle* Anschrift. F\u00fcr historische\nStabilit\u00e4t f\u00fchrt [`crate::domain::Delivery`] zus\u00e4tzlich einen\n`delivery_address_snapshot` \u2014 Adress-\u00c4nderungen wirken nicht\nr\u00fcckwirkend auf bereits zugestellte oder geplante Lieferungen.", "required": [ "id", "erpCustomerId", "name", "address" ], "properties": { "address": { "$ref": "#/components/schemas/Address" }, "erpCustomerId": { "type": "integer", "format": "int64" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" } } }, "CustomerContact": { "type": "object", "description": "Ansprechpartner eines Kunden. Ein Kunde kann mehrere Kontaktpersonen\nhaben (z. B. Empfang vor Ort + Gesch\u00e4ftsf\u00fchrung). Eine Lieferung w\u00e4hlt\n0..N davon als aktive Kontakte aus (siehe\n`Delivery::contact_person_ids`).", "required": [ "id", "customerId", "name" ], "properties": { "customerId": { "type": "string", "format": "uuid" }, "email": { "type": [ "string", "null" ] }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "phone": { "type": [ "string", "null" ] } } }, "DeliveredBelegnummernResponse": { "type": "object", "required": [ "day", "count", "belegnummern" ], "properties": { "belegnummern": { "type": "array", "items": { "type": "string" }, "description": "Belegnummern aller **ausgelieferten** (abgeschlossenen) Lieferungen,\nderen Liefermail noch **nicht versendet** wurde, aufsteigend nach\nAbschluss-Zeitpunkt." }, "count": { "type": "integer", "description": "Anzahl der offenen (noch nicht versendeten) Belege.", "minimum": 0 }, "day": { "type": "string", "description": "Tag, nach dem gefiltert wurde (ISO `YYYY-MM-DD`), oder `\"all\"` wenn kein\n`day` angegeben war." } } }, "Delivery": { "type": "object", "description": "Eine einzelne Lieferung an einen Kunden. Aggregat-Wurzel f\u00fcr die\nLiefer-Items, Notizen und das ggf. zugeordnete Fahrzeug.", "required": [ "id", "tourId", "erpBelegartId", "erpBelegnummer", "customerId", "deliveryAddressSnapshot", "contactPersonIds", "state", "prepaidAmount", "paymentMethodId" ], "properties": { "assignedCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug-Zuordnung, gesetzt in der Ausw\u00e4hlen-Phase.\nBei Ein-Auto-Teams beim Sync automatisch gef\u00fcllt." }, "contactPersonIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Ausgew\u00e4hlte Ansprechpartner f\u00fcr genau diese Lieferung (Auswahl\naus `Customer.contacts`). Kann leer sein." }, "customerId": { "type": "string", "format": "uuid" }, "deliveryAddressSnapshot": { "$ref": "#/components/schemas/Address", "description": "Eingefrorene Liefer-Adresse zum Zeitpunkt des Tour-Syncs.\nSch\u00fctzt vor r\u00fcckwirkenden Kunden-Adress\u00e4nderungen." }, "desiredTime": { "type": [ "string", "null" ], "description": "Wunsch-Lieferzeit als Freitext (z. B. \"vormittags\", \"ab 14:00\")." }, "erpBelegartId": { "type": "integer", "format": "int64", "description": "ERP-Beleg-Bezug: business-stabiles Paar `(Belegart, Belegnummer)`.\n\u00dcberlebt den Belegkopf-Archiv\u00fcbergang." }, "erpBelegnummer": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "paymentMethodId": { "type": "string", "format": "uuid", "description": "F\u00fcr den Restbetrag gew\u00e4hlte Zahlungsart \u2014 FK auf `payment_methods`.\nVom Kunden bei Bestellung festgelegt, der Fahrer \u00fcbernimmt nur\ndie Abwicklung. Aktiv-Flag und Anzeige-Name werden \u00fcber die\nStammdaten-Tabelle aufgel\u00f6st, nicht hier embeddet." }, "prepaidAmount": { "type": "number", "format": "double", "description": "Bei Bestellung schon bezahlter Betrag in EUR. `0.0` wenn der\nKunde alles bei Lieferung zahlt. Wird vom ERP-Sync gef\u00fcllt." }, "specialAgreements": { "type": [ "string", "null" ], "description": "Sondervereinbarungen (z. B. \u201eT\u00fcrklingel defekt, hintenrum klopfen\")." }, "state": { "$ref": "#/components/schemas/DeliveryState" }, "stateReason": { "type": [ "string", "null" ], "description": "Begr\u00fcndung bei `state == Held` oder `state == Canceled`. Beim\nResume / Complete wieder `None`." }, "tourId": { "type": "string", "format": "uuid" } } }, "DeliveryCredit": { "type": "object", "description": "Aktuelle Betrags-Gutschrift einer Lieferung (Geld-Nachlass, unabh\u00e4ngig von\nSt\u00fcckzahl). Abgeleitet aus dem j\u00fcngsten Ereignis im append-only\n`delivery_credit_audit`; existiert nur, solange der letzte Stand `set`\n(und nicht `remove`) ist. `delivery_id` macht den Eintrag \u2014 wie eine\nNotiz \u2014 clientseitig per Lieferung join-bar.", "required": [ "deliveryId", "amountCents", "reason" ], "properties": { "amountCents": { "type": "integer", "format": "int64", "description": "Gutschrift-Betrag in Cent (> 0, \u2264 15000)." }, "deliveryId": { "type": "string", "format": "uuid" }, "reason": { "type": "string" } } }, "DeliveryCreditEventRequest": { "type": "object", "required": [ "clientEventId", "action" ], "properties": { "action": { "$ref": "#/components/schemas/CreditAction" }, "amountCents": { "type": [ "integer", "null" ], "format": "int64", "description": "Bei `Set` Pflicht: Betrag in Cent (> 0, \u2264 15000, Vielfaches von 1000)." }, "authorCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug des Akteurs (Audit-Spur). Muss zum Account geh\u00f6ren." }, "clientEventId": { "type": "string", "format": "uuid", "description": "Idempotenz-Schl\u00fcssel \u2014 pro erzeugtem Ereignis genau einmal vergeben.\nEin Retry mit derselben Id wendet nichts erneut an." }, "reason": { "type": [ "string", "null" ], "description": "Bei `Set` Pflicht: Begr\u00fcndung." } } }, "DeliveryCreditResponse": { "type": "object", "properties": { "credit": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DeliveryCredit", "description": "Aktueller Stand nach dem Ereignis \u2014 `None`, wenn (zuletzt) entfernt." } ] } } }, "DeliveryDetails": { "type": "object", "required": [ "tour", "delivery", "customerContacts", "articles", "warehouses", "notes", "services", "deliveryServices", "contactSources", "contactChannels" ], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/Article" }, "description": "Nur die von den Positionen referenzierten Artikel." }, "contactChannels": { "type": "array", "items": { "$ref": "#/components/schemas/ContactChannel" }, "description": "Die zu `contactSources` geh\u00f6renden Einzel-Kan\u00e4le. Join per `sourceId`." }, "contactSources": { "type": "array", "items": { "$ref": "#/components/schemas/ContactSource" }, "description": "Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner \u2026)." }, "credit": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DeliveryCredit", "description": "Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv)." } ] }, "customer": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/Customer", "description": "Der Kunde der Lieferung (`null`, falls unauffindbar \u2014 sollte nicht\nvorkommen)." } ] }, "customerContacts": { "type": "array", "items": { "$ref": "#/components/schemas/CustomerContact" }, "description": "Ansprechpartner des Kunden." }, "delivery": { "$ref": "#/components/schemas/DeliveryWithItems", "description": "Die Lieferung selbst inkl. Positionen (mit Scan-St\u00e4nden) und `sortOrder`." }, "deliveryServices": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryServiceValue" }, "description": "F\u00fcr diese Lieferung gesetzte Service-Werte." }, "notes": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryNote" }, "description": "Notizen der Lieferung, aufsteigend nach `createdAt`." }, "payment": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/DeliveryPayment", "description": "J\u00fcngste protokollierte Zahlungsabwicklung (`None` = keine)." } ] }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Aktive Service-Definitionen (Stammdaten)." }, "tour": { "$ref": "#/components/schemas/Tour", "description": "Die Tour, zu der die Lieferung geh\u00f6rt." }, "warehouses": { "type": "array", "items": { "$ref": "#/components/schemas/Warehouse" }, "description": "Nur die von den Positionen referenzierten Lager." } } }, "DeliveryItem": { "type": "object", "description": "Einzelposition einer Lieferung. Vereint regul\u00e4re Belegzeilen und\nSt\u00fccklisten-Komponenten zu einer flachen Liste \u2014 die St\u00fccklisten-\nHierarchie ist ein ERP-Konstrukt und wird beim Sync aufgel\u00f6st.\n\n\u00dcber die Felder `belegzeilen_nr` und `komponenten_artikel_nr` bleibt\ndie ERP-Herkunft aufl\u00f6sbar.", "required": [ "id", "deliveryId", "articleId", "requiredQuantity", "warehouseId", "unitPrice", "belegzeilenNr", "scanState" ], "properties": { "articleId": { "type": "string", "format": "uuid" }, "belegzeilenNr": { "type": "integer", "format": "int32", "description": "ERP-Belegzeilen-Nr (Position innerhalb des Belegs)." }, "deliveryId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "komponentenArtikelNr": { "type": [ "string", "null" ], "description": "Bei Items aus einer St\u00fcckliste: Artikelnummer der Komponente.\nBei regul\u00e4ren Belegzeilen: `None`." }, "parentArtikelNr": { "type": [ "string", "null" ], "description": "Artikelnummer des Oberartikels, zu dem diese Komponente geh\u00f6rt.\n`None` bei Oberartikeln/regul\u00e4ren Zeilen \u2014 die App r\u00fcckt Komponenten\ndar\u00fcber unter ihrem Oberartikel ein." }, "requiredQuantity": { "type": "integer", "format": "int32" }, "scanState": { "$ref": "#/components/schemas/ScanState" }, "unitPrice": { "type": "number", "format": "double", "description": "St\u00fcckpreis (brutto, EUR) aus dem ERP-Sync. Der Warenwert einer\nLieferung = \u03a3 `unit_price` \u00d7 ausgelieferte Menge." }, "warehouseId": { "type": "string", "format": "uuid" } } }, "DeliveryNote": { "type": "object", "description": "Notiz an einer Lieferung \u2014 frei eingegeben durch den Fahrer.\n\nMindestens eines von `text` oder `image_attachment` muss gesetzt\nsein. Die Constraint sitzt sowohl im DB-Schema (CHECK) als auch\nin der Application-Schicht.", "required": [ "id", "deliveryId", "authorPersonalnummer", "isAmountCreditNote", "createdAt" ], "properties": { "authorCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug, falls bekannt \u2014 nullable bis das Backend Cars verwaltet." }, "authorPersonalnummer": { "type": "integer", "format": "int64", "description": "Personalnummer des Akteurs (aus dem JWT). Pflicht." }, "createdAt": { "type": "string", "format": "date-time" }, "creditDeliveryItemId": { "type": [ "string", "null" ], "format": "uuid", "description": "Wenn die Notiz als Gutschrift-Grund zu einer Belegzeile angelegt\nwurde: deren `DeliveryItem`-Id. Erlaubt dem Client, die Notiz beim\nZur\u00fccknehmen der Gutschrift (Unremove) gezielt wieder zu l\u00f6schen.\n`None` bei normalen Text-/Foto-Notizen." }, "deliveryId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "imageAttachment": { "type": [ "string", "null" ], "description": "Referenz auf einen Bild-Anhang (z. B. Object-Storage-Key/URL)." }, "imageAttachmentDeleted": { "type": "boolean", "description": "`true`, wenn die lokale Bilddatei nach erfolgreichem Report-Upload\ngel\u00f6scht wurde (das Bild steckt nun im Lieferbericht in DOCUframe).\nRead-only; die App zeigt dann statt der Vorschau einen Hinweis.\nBei Text-Notizen / vorhandenem Bild: `false`." }, "isAmountCreditNote": { "type": "boolean", "description": "`true`, wenn die Notiz den Grund einer **Betrags-Gutschrift**\n(Geld-Nachlass, Lieferungs-Ebene) dokumentiert. Erlaubt dem Client,\nsie beim Entfernen der Gutschrift gezielt zu l\u00f6schen." }, "text": { "type": [ "string", "null" ] } } }, "DeliveryNoteResponse": { "type": "object", "required": [ "note" ], "properties": { "note": { "$ref": "#/components/schemas/DeliveryNote" } } }, "DeliveryOrderEntry": { "type": "object", "required": [ "deliveryId", "sortOrder" ], "properties": { "deliveryId": { "type": "string", "format": "uuid" }, "sortOrder": { "type": "integer", "format": "int32" } } }, "DeliveryPayment": { "type": "object", "description": "Protokollierte Zahlungsabwicklung (\u201eAbkassieren\") einer Lieferung.\n\nAppend-only: jede Best\u00e4tigung des Fahrers ist ein eigener Eintrag; im\nTour-Aggregat wird pro Lieferung nur der j\u00fcngste mitgeliefert. Der Betrag\nist der server-seitig berechnete offene Betrag zum Zeitpunkt der\nBest\u00e4tigung. Weicht der aktuelle offene Betrag davon ab (z. B. nach einer\nneuen Gutschrift), ist der Eintrag veraltet und muss erneuert werden.", "required": [ "id", "deliveryId", "paymentMethodId", "paymentMethodCode", "amountCents", "recordedByPersonalnummer", "recordedAt" ], "properties": { "amountCents": { "type": "integer", "format": "int64", "description": "Abgewickelter Betrag in Cent (offener Betrag bei Best\u00e4tigung)." }, "deliveryId": { "type": "string", "format": "uuid" }, "id": { "type": "string", "format": "uuid" }, "paymentMethodCode": { "type": "string", "description": "Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, \u2026)." }, "paymentMethodId": { "type": "string", "format": "uuid" }, "recordedAt": { "type": "string", "format": "date-time" }, "recordedByCarId": { "type": [ "string", "null" ], "format": "uuid" }, "recordedByPersonalnummer": { "type": "integer", "format": "int64" } } }, "DeliveryPaymentResponse": { "type": "object", "required": [ "payment" ], "properties": { "payment": { "$ref": "#/components/schemas/DeliveryPayment" } } }, "DeliveryResponse": { "type": "object", "required": [ "delivery" ], "properties": { "delivery": { "$ref": "#/components/schemas/Delivery" } } }, "DeliveryServiceResponse": { "type": "object", "required": [ "value" ], "properties": { "value": { "$ref": "#/components/schemas/DeliveryServiceValue" } } }, "DeliveryServiceValue": { "type": "object", "description": "Pro-Lieferung gew\u00e4hlter Wert eines Service. Genau einer der beiden\nWert-Slots ist je nach `ServiceKind` gesetzt; per `service_id`/`delivery_id`\nclientseitig join-bar (wie Notizen/Gutschriften).", "required": [ "deliveryId", "serviceId" ], "properties": { "boolValue": { "type": [ "boolean", "null" ] }, "deliveryId": { "type": "string", "format": "uuid" }, "numericValue": { "type": [ "integer", "null" ], "format": "int32" }, "serviceId": { "type": "string", "format": "uuid" } } }, "DeliveryState": { "type": "string", "description": "Lebenszyklus einer Lieferung.\n\n`Held` ist f\u00fcr \u201eheute nicht zustellbar, aber nicht endg\u00fcltig abgesagt\"\nreserviert; `Canceled` ist endg\u00fcltig. `Completed` setzt der\nAbschluss-Flow am Ende der Auslieferung.", "enum": [ "active", "held", "canceled", "completed" ] }, "DeliveryWithItems": { "allOf": [ { "$ref": "#/components/schemas/Delivery" }, { "type": "object", "required": [ "sortOrder", "items" ], "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryItem" } }, "sortOrder": { "type": "integer", "format": "int32", "description": "Sortier-Reihenfolge innerhalb der Tour (1-basiert)." } } } ] }, "HoldDeliveryRequest": { "type": "object", "required": [ "reason" ], "properties": { "reason": { "type": "string" } } }, "ImportSummary": { "type": "object", "description": "Ergebnis eines Import-Laufs \u2014 pro Fahrer-Tour Erfolg/Fehler getrennt,\ndamit ein einzelner kaputter Beleg nicht den ganzen Tag blockiert.", "required": [ "date", "toursTotal", "toursOk", "toursFailed", "errors" ], "properties": { "date": { "type": "string", "format": "date" }, "driversProvisioned": { "type": "integer", "description": "Anzahl der **neu** im Identity-Provider (Keycloak) angelegten\nFahrer-Konten in diesem Lauf (0, wenn Provisionierung deaktiviert ist\noder alle Konten bereits existierten).", "minimum": 0 }, "errors": { "type": "array", "items": { "type": "string" }, "description": "Fehlertexte je fehlgeschlagener Fahrer-Tour (z. B. unbekannter Fahrer\n\u2192 FK auf `accounts`, oder Validierungsfehler)." }, "provisioningErrors": { "type": "array", "items": { "type": "string" }, "description": "Fehlertexte der Konto-Provisionierung (Keycloak). Best-effort: ein\nFehler hier blockiert den Touren-Import **nicht**." }, "toursFailed": { "type": "integer", "minimum": 0 }, "toursOk": { "type": "integer", "minimum": 0 }, "toursTotal": { "type": "integer", "minimum": 0 } } }, "MarkMailSentRequest": { "type": "object", "required": [ "belegnummern" ], "properties": { "belegnummern": { "type": "array", "items": { "type": "string" }, "description": "Belegnummern, deren Liefermail erfolgreich versendet wurde und die als\nversendet markiert werden sollen." } } }, "MarkMailSentResponse": { "type": "object", "required": [ "marked" ], "properties": { "marked": { "type": "integer", "format": "int64", "description": "Anzahl frisch markierter (vorher offener) Belege. Bereits markierte\nz\u00e4hlen nicht mit (idempotent).", "minimum": 0 } } }, "PaymentMethod": { "type": "object", "description": "Zahlungs-Stammdatensatz.\n\nBewusst eine Tabelle und kein Enum: neue Anbieter (PayPal, Klarna, \u2026)\nkommen \u00fcber den `POST /payment-methods`-Endpoint hinzu. Domain-Code\nkann trotzdem fachliche Sonderf\u00e4lle \u00fcber den stabilen `code` (z. B.\n`\"invoice\"` braucht Bonit\u00e4tspr\u00fcfung) referenzieren \u2014 die UUID dient\nnur als FK in `deliveries`.\n\n`active = false` ist Soft-Delete: die Methode bleibt referenzierbar\nf\u00fcr historische Lieferungen, taucht aber in der UI-Auswahl nicht\nmehr auf. Echtes L\u00f6schen ist nur m\u00f6glich, wenn keine Lieferung sie\nreferenziert \u2014 Datenbank-Constraint regelt das via\n`ON DELETE RESTRICT`.", "required": [ "id", "code", "name", "active", "createdAt" ], "properties": { "active": { "type": "boolean" }, "code": { "type": "string", "description": "Stabiler Programm-Identifier \u2014 z. B. `\"cash\"`, `\"ec_card\"`.\nEindeutig pro Eintrag. Wird vom Aufrufer beim Anlegen gesetzt." }, "createdAt": { "type": "string", "format": "date-time" }, "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "description": "Display-Name in der UI \u2014 frei via PATCH \u00e4nderbar." } } }, "PaymentMethodResponse": { "type": "object", "required": [ "method" ], "properties": { "method": { "$ref": "#/components/schemas/PaymentMethod" } } }, "PaymentMethodsList": { "type": "object", "required": [ "methods" ], "properties": { "methods": { "type": "array", "items": { "$ref": "#/components/schemas/PaymentMethod" } } } }, "PendingReviewResponse": { "type": "object", "required": [ "delivery_id", "erp_belegart_id", "erp_belegnummer", "customer_name", "tour_date", "last_change_at", "credited_items", "money_credit_cents" ], "properties": { "credited_items": { "type": "array", "items": { "$ref": "#/components/schemas/ReviewedItemResponse" }, "description": "Entfernte/teil-gutgeschriebene Positionen." }, "customer_name": { "type": "string" }, "delivery_id": { "type": "string" }, "erp_belegart_id": { "type": "integer", "format": "int64" }, "erp_belegnummer": { "type": "string" }, "last_change_at": { "type": "string", "description": "Zeitpunkt der letzten beleg-\u00e4ndernden Aktion (RFC 3339)." }, "money_credit_cents": { "type": "integer", "format": "int64", "description": "Geld-Gutschrift in Cent (0 = keine)." }, "money_credit_reason": { "type": [ "string", "null" ] }, "tour_date": { "type": "string", "description": "Tourdatum (ISO `YYYY-MM-DD`)." } } }, "PositionsModifiedResponse": { "type": "object", "required": [ "belegnummer", "positions_modified" ], "properties": { "belegnummer": { "type": "string", "description": "ERP-Belegnummer, nach der gefragt wurde." }, "positions_modified": { "type": "boolean", "description": "`true`, wenn an der Lieferung Positionen ver\u00e4ndert wurden \u2014 eine Zeile\nwurde entfernt oder in der Menge reduziert (St\u00fcck-Gutschrift) **oder** es\nliegt eine aktive Geld-Gutschrift vor." } } }, "RecordDeliveryPaymentRequest": { "type": "object", "required": [ "clientEventId", "paymentMethodId", "expectedAmountCents" ], "properties": { "authorCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug des Akteurs (Audit-Spur). Muss zum Account geh\u00f6ren." }, "clientEventId": { "type": "string", "format": "uuid", "description": "Idempotenz-Schl\u00fcssel \u2014 pro Best\u00e4tigung genau einmal vergeben." }, "expectedAmountCents": { "type": "integer", "format": "int64", "description": "Betrag in Cent, den der Fahrer in der App gesehen und best\u00e4tigt hat.\nDer Server berechnet den offenen Betrag selbst und lehnt ab, wenn er\ndavon abweicht \u2014 so landet nie ein Betrag im Protokoll, den der\nFahrer nicht gesehen hat." }, "paymentMethodId": { "type": "string", "format": "uuid", "description": "Gew\u00e4hlte Zahlungsmethode. Muss existieren und aktiv sein." } } }, "ResolveReviewRequest": { "type": "object", "required": [ "resolved_by" ], "properties": { "note": { "type": [ "string", "null" ], "description": "Optionale Notiz zur getroffenen Entscheidung." }, "resolved_by": { "type": "string", "description": "Bearbeiter (Name/K\u00fcrzel), der die Pr\u00fcfung best\u00e4tigt." } } }, "ReviewedItemResponse": { "type": "object", "required": [ "belegzeilen_nr", "artikel_nr", "article_name", "required_quantity", "credited_quantity" ], "properties": { "article_name": { "type": "string" }, "artikel_nr": { "type": "string" }, "belegzeilen_nr": { "type": "integer", "format": "int32" }, "credited_quantity": { "type": "integer", "format": "int32" }, "reason": { "type": [ "string", "null" ] }, "required_quantity": { "type": "integer", "format": "int32" } } }, "ScanEvent": { "type": "object", "required": [ "clientScanId", "deliveryItemId", "action", "clientScannedAt" ], "properties": { "action": { "$ref": "#/components/schemas/AuditAction" }, "actorCarId": { "type": [ "string", "null" ], "format": "uuid", "description": "Fahrzeug, in dem der Scan gemacht wurde. Muss zum\nangemeldeten Account geh\u00f6ren. `None` ist erlaubt, schw\u00e4cht\naber den Audit-Trail." }, "clientScanId": { "type": "string", "format": "uuid" }, "clientScannedAt": { "type": "string", "format": "date-time" }, "deliveryItemId": { "type": "string", "format": "uuid" }, "manual": { "type": "boolean", "description": "`true`, wenn der Fahrer die Position **manuell** als geladen best\u00e4tigt\nhat (Fallback ohne Barcode-Scan). Wird nur im Audit (`scan_audit.manual`)\nfestgehalten; an der Mengen-/Status-Logik \u00e4ndert es nichts. Default\n`false` (regul\u00e4rer Barcode-Scan)." }, "quantity": { "type": [ "integer", "null" ], "format": "int32", "description": "Menge f\u00fcr `Remove` / `Unremove` (Mengen-Gutschrift): wie viele St\u00fcck\nder Belegzeile gutgeschrieben bzw. wieder hergestellt werden.\n`None` = ganze Restmenge (abw\u00e4rtskompatibel zum bisherigen\n\u201eganze Zeile entfernen\"). Bei `Scan`/`Unscan`/`Hold`/`Unhold`\nignoriert. Muss, wenn gesetzt, `> 0` sein." }, "reason": { "type": [ "string", "null" ], "description": "Pflicht bei `Hold` und `Remove`. Sonst ignoriert." } } }, "ScanResult": { "type": "object", "required": [ "clientScanId", "status" ], "properties": { "clientScanId": { "type": "string", "format": "uuid" }, "deliveryItemId": { "type": [ "string", "null" ], "format": "uuid", "description": "Aktueller `scan_state` der Position nach der Verarbeitung \u2014\ngenau dann gesetzt, wenn der Server den Stand kennen konnte\n(`Applied` oder `Duplicate`). Erlaubt der App, die UI ohne\nRe-Fetch zu aktualisieren." }, "newScanState": { "oneOf": [ { "type": "null" }, { "$ref": "#/components/schemas/ScanState" } ] }, "reason": { "type": [ "string", "null" ], "description": "Bei `Rejected`: Begr\u00fcndung. Bei `Applied`/`Duplicate`: `None`." }, "status": { "$ref": "#/components/schemas/ScanResultStatus" } } }, "ScanResultStatus": { "type": "string", "enum": [ "applied", "duplicate", "rejected" ] }, "ScanState": { "type": "object", "description": "Eingebetteter Scan-Zustand pro [`DeliveryItem`]. Wird durch\n`ScanAuditEntry`-Events fortgeschrieben \u2014 das Audit-Log ist die\nWahrheit \u00fcber das WIE und WANN, dieses Embedded-VO ist die schnelle\nWahrheit \u00fcber das WIEVIEL.", "required": [ "scannedQuantity", "creditedQuantity", "status", "lastUpdatedAt" ], "properties": { "creditedQuantity": { "type": "integer", "format": "int32", "description": "Als Gutschrift entfernte Menge (0..=required_quantity). Eigene\nDimension neben `scanned_quantity`: \u201ewie viele St\u00fcck dieser Zeile hat\nder Kunde nicht angenommen\". `status == Removed` entspricht\n`credited_quantity == required_quantity` (ganze Zeile gutgeschrieben)." }, "heldReason": { "type": [ "string", "null" ], "description": "Grund bei `status == Held` oder `status == Removed`." }, "lastUpdatedAt": { "type": "string", "format": "date-time" }, "scannedQuantity": { "type": "integer", "format": "int32" }, "status": { "$ref": "#/components/schemas/ScanStatus" } } }, "ScanStatus": { "type": "string", "description": "Status einer einzelnen Scan-Position innerhalb eines Items.", "enum": [ "in_progress", "done", "held", "removed" ] }, "Service": { "type": "object", "description": "Service-Stammdatensatz \u2014 admin-konfigurierbar (Muster wie `PaymentMethod`).\n\n`key` ist der stabile Programm-Identifier (eindeutig), `name` der\nAnzeige-Name. `min_value`/`max_value` sind nur f\u00fcr `Numeric` relevant.\n`active = false` ist Soft-Delete (bleibt f\u00fcr historische Lieferungen\nreferenzierbar, f\u00e4llt aus dem Default-Listing).", "required": [ "id", "key", "name", "kind", "active", "sortOrder" ], "properties": { "active": { "type": "boolean" }, "id": { "type": "string", "format": "uuid" }, "key": { "type": "string" }, "kind": { "$ref": "#/components/schemas/ServiceKind" }, "maxValue": { "type": [ "integer", "null" ], "format": "int32" }, "minValue": { "type": [ "integer", "null" ], "format": "int32" }, "name": { "type": "string" }, "sortOrder": { "type": "integer", "format": "int32" } } }, "ServiceKind": { "type": "string", "description": "Eingabetyp eines Service (fr\u00fcher \u201eLieferoption\"). `Boolean` rendert als\nCheckbox, `Numeric` als Zahlenfeld mit optionalen Grenzen.", "enum": [ "boolean", "numeric" ] }, "ServiceResponse": { "type": "object", "required": [ "service" ], "properties": { "service": { "$ref": "#/components/schemas/Service" } } }, "ServicesList": { "type": "object", "required": [ "services" ], "properties": { "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } } } }, "SetDeliveryOrderRequest": { "type": "object", "required": [ "deliveryIds" ], "properties": { "deliveryIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Reihenfolge: Position im Array (0-basiert) wird zu `sort_order`\n(1-basiert) gemappt." } } }, "SetDeliveryOrderResponse": { "type": "object", "required": [ "tourId", "order" ], "properties": { "order": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryOrderEntry" } }, "tourId": { "type": "string", "format": "uuid" } } }, "SetDeliveryServiceRequest": { "type": "object", "description": "Setzt den Wert eines Service f\u00fcr eine Lieferung (Upsert). Es muss genau\ndas zum `ServiceKind` passende Feld gesetzt sein (Use Case pr\u00fcft das).", "properties": { "authorCarId": { "type": [ "string", "null" ], "format": "uuid" }, "boolValue": { "type": [ "boolean", "null" ] }, "numericValue": { "type": [ "integer", "null" ], "format": "int32" } } }, "SyncContactChannel": { "type": "object", "required": [ "kind", "position", "value" ], "properties": { "kind": { "$ref": "#/components/schemas/ContactKind" }, "position": { "type": "integer", "format": "int32", "description": "1-basiert: spiegelt ERP-Spaltenposition (Telefon \u2192 1, Telefon2 \u2192 2, \u2026)." }, "value": { "type": "string" } } }, "SyncContactSource": { "type": "object", "description": "Eine Adress-Rolle eines Belegs mit Namensblock und allen ausgef\u00fcllten\nTelefon-/Mobil-/E-Mail-/Web-Eintr\u00e4gen.", "required": [ "role" ], "properties": { "abteilung": { "type": [ "string", "null" ] }, "anrede": { "type": [ "string", "null" ] }, "channels": { "type": "array", "items": { "$ref": "#/components/schemas/SyncContactChannel" } }, "funktion": { "type": [ "string", "null" ] }, "name1": { "type": [ "string", "null" ] }, "name2": { "type": [ "string", "null" ] }, "name3": { "type": [ "string", "null" ] }, "role": { "$ref": "#/components/schemas/ContactRole" }, "titel": { "type": [ "string", "null" ] } } }, "SyncDelivery": { "type": "object", "required": [ "belegartId", "belegnummer", "erpCustomerId", "customerName", "customerAddress", "deliveryAddress", "sortOrder", "items" ], "properties": { "belegartCode": { "type": [ "string", "null" ], "description": "Belegart-Kurzcode (z. B. \u201eVL5\"), aus `Belegarten.Belegart` (getrimmt)." }, "belegartId": { "type": "integer", "format": "int64" }, "belegartName": { "type": [ "string", "null" ], "description": "Belegart-Klartext (z. B. \u201eLieferschein EH\"), aus `Belegarten.Bezeichnung`." }, "belegnummer": { "type": "string" }, "contactSources": { "type": "array", "items": { "$ref": "#/components/schemas/SyncContactSource" }, "description": "Alle vom ERP an diesem Beleg h\u00e4ngenden Kontakt-Adressen (Beleg-/\nLiefer-/Rechnungsadresse, Ansprechpartner, Kundenstamm). Leere\nQuellen (kein einziger ausgef\u00fcllter Kanal *und* kein Name) l\u00e4sst\nder Sync weg." }, "customerAddress": { "$ref": "#/components/schemas/Address" }, "customerName": { "type": "string" }, "deliveryAddress": { "$ref": "#/components/schemas/Address", "description": "Snapshot der Lieferadresse (kann von der Stammadresse abweichen)." }, "desiredTime": { "type": [ "string", "null" ] }, "erpCustomerId": { "type": "integer", "format": "int64" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/SyncDeliveryItem" } }, "paymentMethodCode": { "type": [ "string", "null" ], "description": "F\u00fcr den Restbetrag gew\u00e4hlte Zahlungsart \u2014 Referenz per\n`code` (z. B. `\"cash\"`, `\"invoice\"`). Das ERP kennt seine\nStandard-Codes, der Sync-Code resolvet sie zur UUID. Wenn\n`None`, f\u00e4llt der Backend-Code auf `\"cash\"` zur\u00fcck." }, "prepaidAmount": { "type": "number", "format": "double", "description": "Bei Bestellung schon bezahlter Betrag in EUR. Default `0.0`,\nwenn der Kunde alles bei Lieferung zahlt. Der ERP-Sync liefert\nden Wert mit." }, "sortOrder": { "type": "integer", "format": "int32", "description": "1-basiert, definiert die initiale Reihenfolge in der App." }, "specialAgreements": { "type": [ "string", "null" ] } } }, "SyncDeliveryItem": { "type": "object", "required": [ "belegzeilenNr", "articleNumber", "articleName", "articleScannable", "warehouseCode", "warehouseName", "requiredQuantity" ], "properties": { "articleDefaultWarehouseCode": { "type": [ "string", "null" ], "description": "Default-Lager-Code f\u00fcr den Artikel (Anlage neuer Artikel)." }, "articleName": { "type": "string" }, "articleNumber": { "type": "string" }, "articleScannable": { "type": "boolean" }, "belegzeilenNr": { "type": "integer", "format": "int32" }, "komponentenArtikelNr": { "type": [ "string", "null" ], "description": "Komponenten-Artikelnummer bei aufgel\u00f6sten St\u00fccklisten, sonst leer.\nTr\u00e4gt die **eigene** Nummer der Komponente (eindeutig je Belegzeile)." }, "parentArtikelNr": { "type": [ "string", "null" ], "description": "Artikelnummer des **Oberartikels**, zu dem diese Komponente geh\u00f6rt.\n`None` bei Oberartikeln/regul\u00e4ren Zeilen. Erlaubt der App, Komponenten\nunter ihrem Oberartikel einzur\u00fccken." }, "requiredQuantity": { "type": "integer", "format": "int32" }, "unitPrice": { "type": "number", "format": "double", "description": "St\u00fcckpreis (brutto, EUR). Default `0.0`. Liefert der ERP-Sync mit;\ndie App rechnet daraus den Warenwert." }, "warehouseCode": { "type": "string" }, "warehouseName": { "type": "string" } } }, "SyncTourRequest": { "type": "object", "required": [ "driverPersonalnummer", "tourDate", "deliveries" ], "properties": { "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/SyncDelivery" } }, "driverPersonalnummer": { "type": "integer", "format": "int64" }, "tourDate": { "type": "string", "format": "date" } } }, "SyncTourResponse": { "type": "object", "description": "Antwort-H\u00fclle f\u00fcr `POST /sync/tour`.", "required": [ "tourId" ], "properties": { "tourId": { "type": "string", "format": "uuid" } } }, "Tour": { "type": "object", "description": "Tour eines Tages, pro [`crate::domain::Account`]. Aggregat-Wurzel\nf\u00fcr die Lieferungen dieses Tages \u2014 die einzelnen [`crate::domain::Delivery`]\nreferenzieren ihre Tour per FK.\n\nDer Sync vom ERP l\u00e4uft in der Regel einmal am Vortag und f\u00fcllt eine\nneue Tour-Zeile inklusive Delivery- und DeliveryItem-Strukturen.", "required": [ "id", "accountId", "date", "syncedAt" ], "properties": { "accountId": { "type": "integer", "format": "int64" }, "date": { "type": "string", "format": "date" }, "id": { "type": "string", "format": "uuid" }, "syncedAt": { "type": "string", "format": "date-time", "description": "Zeitpunkt des letzten ERP-Sync \u2014 f\u00fcr Drift-Erkennung." } } }, "TourDetails": { "type": "object", "required": [ "tour", "deliveries", "customers", "customerContacts", "articles", "warehouses", "notes", "credits", "payments", "services", "deliveryServices", "contactSources", "contactChannels" ], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/Article" } }, "contactChannels": { "type": "array", "items": { "$ref": "#/components/schemas/ContactChannel" }, "description": "Die zu `contact_sources` geh\u00f6renden Einzel-Kan\u00e4le (Telefon, Mobil,\nE-Mail, Web). Join per `source_id`." }, "contactSources": { "type": "array", "items": { "$ref": "#/components/schemas/ContactSource" }, "description": "Alle vom ERP gespiegelten Kontaktquellen aller Lieferungen dieser\nTour. Die App joint clientseitig per `delivery_id` und gruppiert\nnach `role` (Lieferadresse / Rechnungsadresse / Ansprechpartner /\nKundenstamm / Belegadresse)." }, "credits": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryCredit" }, "description": "Aktuelle Betrags-Gutschriften (j\u00fcngster Stand pro Lieferung), nur f\u00fcr\nLieferungen, deren letztes Ereignis `set` war. Join per `delivery_id`." }, "customerContacts": { "type": "array", "items": { "$ref": "#/components/schemas/CustomerContact" } }, "customers": { "type": "array", "items": { "$ref": "#/components/schemas/Customer" } }, "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryWithItems" } }, "deliveryServices": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryServiceValue" }, "description": "Pro-Lieferung gesetzte Service-Werte. Join per `delivery_id` +\n`service_id`." }, "notes": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryNote" }, "description": "Alle Notizen aller Lieferungen dieser Tour, in einer Liste.\nDie App joint clientseitig per `delivery_id`. Reihenfolge:\npro Lieferung aufsteigend nach `created_at`." }, "payments": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryPayment" }, "description": "J\u00fcngste protokollierte Zahlungsabwicklung pro Lieferung (nur\nLieferungen mit mindestens einem Eintrag). Join per `delivery_id`." }, "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" }, "description": "Aktive Service-Definitionen (Stammdaten) \u2014 die App rendert daraus\nPhase 4. Bewusst hier mitgeliefert, damit die Detailseite alles aus\ndem Tour-Aggregat hat." }, "tour": { "$ref": "#/components/schemas/Tour" }, "warehouses": { "type": "array", "items": { "$ref": "#/components/schemas/Warehouse" } } } }, "TourSummary": { "type": "object", "required": [ "tourId", "tourDate", "deliveryCount" ], "properties": { "deliveryCount": { "type": "integer", "format": "int64" }, "tourDate": { "type": "string", "format": "date" }, "tourId": { "type": "string", "format": "uuid" } } }, "TourSummaryList": { "type": "object", "description": "Antwort-H\u00fclle f\u00fcr `GET /me/tours/today`. Eigenes Struct, weil\nutoipa f\u00fcr `Vec` als Top-Level-Response keinen sauberen\nSchemanamen vergibt \u2014 und ein Wrapper macht die Erweiterbarkeit\n(z. B. Paginierung in Zukunft) zur Nicht-Breaking-Change.", "required": [ "tours" ], "properties": { "tours": { "type": "array", "items": { "$ref": "#/components/schemas/TourSummary" } } } }, "UpdateCarRequest": { "type": "object", "properties": { "active": { "type": [ "boolean", "null" ], "description": "Wenn gesetzt: aktiv/inaktiv. Inaktive Fahrzeuge tauchen in\n`GET /me/cars?activeOnly=true` (default) nicht auf." }, "plate": { "type": [ "string", "null" ], "description": "Wenn gesetzt: neues Kennzeichen." } } }, "UpdateDeliveryNoteRequest": { "type": "object", "description": "Request f\u00fcr `PATCH /deliveries/{id}/notes/{note_id}`. Wie beim Create\nmuss mindestens eines von `text` / `image_attachment` inhaltlich gef\u00fcllt\nsein \u2014 gepr\u00fcft im Use Case.", "properties": { "imageAttachment": { "type": [ "string", "null" ], "description": "Object-Storage-Key oder URL eines vorab hochgeladenen Bildes." }, "text": { "type": [ "string", "null" ] } } }, "UpdatePaymentMethodRequest": { "type": "object", "properties": { "active": { "type": [ "boolean", "null" ], "description": "Wenn gesetzt: aktiv/inaktiv. Inaktive Methoden bleiben f\u00fcr\nhistorische Lieferungen referenzierbar, tauchen aber im\nDefault-Listing nicht auf." }, "name": { "type": [ "string", "null" ], "description": "Wenn gesetzt: neuer Anzeige-Name." } } }, "UpdateServiceRequest": { "type": "object", "description": "Teil-Update eines Service. `kind` ist bewusst **nicht** \u00e4nderbar \u2014 ein\nWechsel boolean\u2194numeric w\u00fcrde bestehende Pro-Lieferung-Werte ung\u00fcltig\nmachen (dann lieber deaktivieren + neu anlegen).", "properties": { "active": { "type": [ "boolean", "null" ] }, "maxValue": { "type": [ "integer", "null" ], "format": "int32" }, "minValue": { "type": [ "integer", "null" ], "format": "int32" }, "name": { "type": [ "string", "null" ] }, "sortOrder": { "type": [ "integer", "null" ], "format": "int32" } } }, "Warehouse": { "type": "object", "description": "Lager. ERP-Mirror; `code` ist die ERP-Lager-Nr (z. B. `\"0\"` f\u00fcr das\nStandardlager). Das `is_standard`-Flag ist der schnelle Filter f\u00fcr\ndie Beladen-Logik (\u201enur Standardlager-Artikel z\u00e4hlen f\u00fcr Fertig\").", "required": [ "id", "code", "name", "isStandard" ], "properties": { "code": { "type": "string" }, "id": { "type": "string", "format": "uuid" }, "isStandard": { "type": "boolean" }, "name": { "type": "string" } } } }, "securitySchemes": { "admin_api_key": { "type": "apiKey", "in": "header", "name": "X-Admin-Api-Key" }, "bearer_auth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } } }, "security": [ { "bearer_auth": [] } ], "tags": [ { "name": "health", "description": "Health- und Status-Endpoints" }, { "name": "accounts", "description": "Account-Stammdaten" }, { "name": "tours", "description": "Touren der Fahrer" }, { "name": "sync", "description": "ERP-Sync-Endpunkte" }, { "name": "scans", "description": "Scan-Events (Beladung & Auslieferung)" }, { "name": "deliveries", "description": "Delivery-Lifecycle (hold / resume / cancel / complete)" }, { "name": "cars", "description": "Fahrzeug-Stammdaten pro Fahrer" }, { "name": "payment-methods", "description": "Zahlungsmethoden \u2014 globale Stammdaten" }, { "name": "services", "description": "Services / Lieferoptionen \u2014 globale Stammdaten" }, { "name": "admin", "description": "Betriebs-/Admin-Endpunkte (Maschinen-Zugang via X-Admin-Api-Key, kein JWT)" } ] }