{ "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": [] } ] } }, "/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`.", "operationId": "complete", "parameters": [ { "name": "delivery_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Lieferung abgeschlossen", "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}/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}/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": [] } ] } }, "/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": [] } ] } }, "/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": [] } ] } }, "/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).", "enum": [ "scan", "unscan", "hold", "unhold", "remove" ] }, "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" } } } }, "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." }, "imageAttachment": { "type": [ "string", "null" ], "description": "Object-Storage-Key oder URL eines vorab hochgeladenen Bildes." }, "text": { "type": [ "string", "null" ] } } }, "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" ] } } }, "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" ], "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" }, "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" } } }, "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", "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`." }, "requiredQuantity": { "type": "integer", "format": "int32" }, "scanState": { "$ref": "#/components/schemas/ScanState" }, "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", "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" }, "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)." }, "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" } } }, "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" } } }, "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" }, "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", "status", "lastUpdatedAt" ], "properties": { "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" ] }, "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" } } }, "SyncDelivery": { "type": "object", "required": [ "belegartId", "belegnummer", "erpCustomerId", "customerName", "customerAddress", "deliveryAddress", "sortOrder", "items" ], "properties": { "belegartId": { "type": "integer", "format": "int64" }, "belegnummer": { "type": "string" }, "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" } }, "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." }, "requiredQuantity": { "type": "integer", "format": "int32" }, "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" ], "properties": { "articles": { "type": "array", "items": { "$ref": "#/components/schemas/Article" } }, "customerContacts": { "type": "array", "items": { "$ref": "#/components/schemas/CustomerContact" } }, "customers": { "type": "array", "items": { "$ref": "#/components/schemas/Customer" } }, "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/DeliveryWithItems" } }, "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`." }, "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." } } }, "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": { "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" } ] }