//! Bulk-Scan-Endpoint: Request, Response, pro-Event-Result. //! //! Idempotenz läuft über `client_scan_id`: ein UUID, das die App pro //! erzeugtem Scan-Event genau einmal vergibt. Retry desselben Events //! liefert `Duplicate` zurück. Pro Event ein eigener kleiner Vorgang — //! ein einzelner Reject blockiert die anderen nicht. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use holzleitner_domain::{AuditAction, ScanState}; #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] pub struct ApplyScansRequest { pub scans: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] pub struct ScanEvent { pub client_scan_id: Uuid, pub delivery_item_id: Uuid, pub action: AuditAction, /// Pflicht bei `Hold` und `Remove`. Sonst ignoriert. pub reason: Option, /// Menge für `Remove` / `Unremove` (Mengen-Gutschrift): wie viele Stück /// der Belegzeile gutgeschrieben bzw. wieder hergestellt werden. /// `None` = ganze Restmenge (abwärtskompatibel zum bisherigen /// „ganze Zeile entfernen"). Bei `Scan`/`Unscan`/`Hold`/`Unhold` /// ignoriert. Muss, wenn gesetzt, `> 0` sein. #[serde(default)] pub quantity: Option, /// `true`, wenn der Fahrer die Position **manuell** als geladen bestätigt /// hat (Fallback ohne Barcode-Scan). Wird nur im Audit (`scan_audit.manual`) /// festgehalten; an der Mengen-/Status-Logik ändert es nichts. Default /// `false` (regulärer Barcode-Scan). #[serde(default)] pub manual: bool, pub client_scanned_at: DateTime, /// Fahrzeug, in dem der Scan gemacht wurde. Muss zum /// angemeldeten Account gehören. `None` ist erlaubt, schwächt /// aber den Audit-Trail. pub actor_car_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] pub struct ApplyScansResponse { pub results: Vec, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "snake_case")] pub enum ScanResultStatus { /// Aktion wurde frisch angewendet; Audit-Eintrag geschrieben. Applied, /// `client_scan_id` war bereits bekannt. Item-State unverändert, /// `scan_state` zeigt den aktuellen Stand am Server. Duplicate, /// Aktion wurde abgelehnt (z. B. unbekanntes Item, invalider /// Statusübergang, fehlender Pflicht-Reason). `reason` füllt die /// Begründung; `scan_state` ist `None`. Rejected, } #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] pub struct ScanResult { pub client_scan_id: Uuid, pub status: ScanResultStatus, /// Bei `Rejected`: Begründung. Bei `Applied`/`Duplicate`: `None`. pub reason: Option, /// Aktueller `scan_state` der Position nach der Verarbeitung — /// genau dann gesetzt, wenn der Server den Stand kennen konnte /// (`Applied` oder `Duplicate`). Erlaubt der App, die UI ohne /// Re-Fetch zu aktualisieren. pub delivery_item_id: Option, pub new_scan_state: Option, }