{ "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/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": [] } ] } }, "/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}/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/today": { "get": { "tags": [ "tours" ], "summary": "Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT).", "operationId": "list_my_tours_today", "responses": { "200": { "description": "Liste der heutigen Touren", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TourSummaryList" } } } }, "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": { "city": { "type": "string" }, "country": { "type": "string" }, "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." }, "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." } } }, "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." } ] } } }, "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" } } }, "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" } } } }, "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", "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`." }, "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)" } ] }