Files
Holzleitner---Backend--aktu…/crates/application/src/dto/scan.rs
Dennis Nemec 6a9b5872e1 Backend-Arbeitsstand: ERP-Sync, Lieferlebenszyklus, Reports + config.toml
Bringt das Backend vom initialen Skeleton auf den aktuellen Arbeitsstand
(Clean Architecture: domain → application → infrastructure → api).

Wesentliche Bereiche:
- ERP-Anbindung (MSSQL-Pull der Touren, Import-Scheduler, Rückschreiben)
- Lieferlebenszyklus: Scan/Hold/Cancel/Complete, Gutschriften, Notizen,
  Bild-Anhänge, Unterschriften, PDF-Lieferreport → DOCUframe
- Stammdaten: Kunden, Artikel, Lager, Zahlungsarten, Services
- Keycloak-JWT-Gate + Fahrer-Provisionierung via Admin-API
- Admin-API-Key-Gate (X-Admin-Api-Key) für Maschinen-Endpunkte

Jüngste Änderungen dieser Session:
- Belegspezifische Kontaktdaten: alle ERP-Adressen (Beleg-/Liefer-/
  Rechnungsadresse, Ansprechpartner, Kundenstamm) mit Telefon/Mobil/
  E-Mail werden gesynct (Migration 0029, MSSQL-Query, TourDetails)
- Konfiguration von .env (envy/dotenvy) auf config.toml (toml/serde)
  umgestellt; Vorlage config.example.toml, Pfad via HOLZLEITNER_CONFIG

Nicht im Repo (per .gitignore): config.toml (Secrets), data/ (Laufzeit-/
Kundendaten), demo.mp4, .claude/, variocontrol-ai/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:52:58 +02:00

87 lines
3.4 KiB
Rust

//! 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<ScanEvent>,
}
#[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<String>,
/// 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<i32>,
/// `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<Utc>,
/// 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<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ApplyScansResponse {
pub results: Vec<ScanResult>,
}
#[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<String>,
/// 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<Uuid>,
pub new_scan_state: Option<ScanState>,
}