diff --git a/crates/application/src/ports/delivery_completion_repository.rs b/crates/application/src/ports/delivery_completion_repository.rs index e398a6b..83991bf 100644 --- a/crates/application/src/ports/delivery_completion_repository.rs +++ b/crates/application/src/ports/delivery_completion_repository.rs @@ -83,6 +83,16 @@ pub trait DeliveryCompletionRepository: Send + Sync { input: CompleteDeliveryInput, ) -> Result; + /// Liefert die ERP-Belegnummer einer Lieferung. Der Abschluss-Use-Case + /// braucht sie, um die Unterschrifts-Dateien nach dem Schema + /// `delivery_{Belegnummer}_signature_{role}.png` zu benennen — also + /// *vor* dem eigentlichen `complete`-Aufruf. `NotFound`, wenn die + /// Lieferung nicht existiert. + async fn belegnummer_for( + &self, + delivery_id: Uuid, + ) -> Result; + /// Lädt die für das ERP-Rückschreiben nötigen Daten einer **bereits /// abgeschlossenen** Lieferung (Beleg-Key, ausgelieferte Mengen, /// Geld-Gutschrift, Abschluss-Zeitpunkt). diff --git a/crates/application/src/ports/signature_storage.rs b/crates/application/src/ports/signature_storage.rs index 469478c..134fe0f 100644 --- a/crates/application/src/ports/signature_storage.rs +++ b/crates/application/src/ports/signature_storage.rs @@ -11,7 +11,6 @@ use std::time::Duration; use async_trait::async_trait; -use uuid::Uuid; use crate::error::ApplicationError; @@ -35,11 +34,13 @@ impl SignatureRole { pub trait SignatureStorage: Send + Sync { /// Speichert eine Unterschrift (PNG-Bytes) für eine Lieferung+Rolle und /// liefert die persistente, relative Referenz (Dateiname) zurück. - /// Deterministisch über `delivery_id`+`role` — ein Retry überschreibt + /// Deterministisch über `belegnummer`+`role` — der Dateiname folgt dem + /// Schema `delivery_{belegnummer}_signature_{role}.png` (z. B. + /// `delivery_V-30690287_signature_customer.png`); ein Retry überschreibt /// dieselbe Datei statt Müll anzuhäufen. async fn save( &self, - delivery_id: Uuid, + belegnummer: &str, role: SignatureRole, bytes: Vec, ) -> Result; @@ -49,12 +50,6 @@ pub trait SignatureStorage: Send + Sync { /// Einbetten in den PDF-Report. `None`, wenn die Datei fehlt. async fn load(&self, reference: &str) -> Result>, ApplicationError>; - /// Löscht beide Unterschriften (Kunde + Fahrer) einer Lieferung. Idempotent - /// (fehlende Datei = ok). Hinweis: Wird NICHT mehr beim Report-Upload - /// aufgerufen — Unterschriften bleiben erhalten und werden per Cron über - /// [`delete_older_than`](Self::delete_older_than) nach Frist entfernt. - async fn delete_for_delivery(&self, delivery_id: Uuid) -> Result<(), ApplicationError>; - /// Löscht alle Unterschrifts-Dateien, deren letzte Änderung länger als /// `max_age` zurückliegt (Aufbewahrungsfrist). Für den periodischen /// Cron-Cleanup. Liefert die Anzahl gelöschter Dateien. diff --git a/crates/application/src/usecases/complete_delivery.rs b/crates/application/src/usecases/complete_delivery.rs index 531550f..94f417b 100644 --- a/crates/application/src/usecases/complete_delivery.rs +++ b/crates/application/src/usecases/complete_delivery.rs @@ -76,13 +76,16 @@ impl CompleteDeliveryUseCase { } // --- Signaturen lokal speichern ----------------------------------- + // Dateiname folgt dem Schema delivery_{Belegnummer}_signature_{role}.png + // → Belegnummer der Lieferung auflösen, bevor geschrieben wird. + let belegnummer = self.repository.belegnummer_for(delivery_id).await?; let customer_signature_path = self .signatures - .save(delivery_id, SignatureRole::Customer, customer_signature_png) + .save(&belegnummer, SignatureRole::Customer, customer_signature_png) .await?; let driver_signature_path = self .signatures - .save(delivery_id, SignatureRole::Driver, driver_signature_png) + .save(&belegnummer, SignatureRole::Driver, driver_signature_png) .await?; // --- Atomarer Abschluss im Repository ----------------------------- diff --git a/crates/infrastructure/src/persistence/delivery_completion_repository.rs b/crates/infrastructure/src/persistence/delivery_completion_repository.rs index 48f4510..1bcfe91 100644 --- a/crates/infrastructure/src/persistence/delivery_completion_repository.rs +++ b/crates/infrastructure/src/persistence/delivery_completion_repository.rs @@ -331,6 +331,19 @@ impl DeliveryCompletionRepository for PgDeliveryCompletionRepository { Ok(build_delivery(row, DeliveryState::Completed, None, contacts)) } + async fn belegnummer_for( + &self, + delivery_id: Uuid, + ) -> Result { + let belegnummer: Option = + sqlx::query_scalar("SELECT erp_belegnummer FROM deliveries WHERE id = $1") + .bind(delivery_id) + .fetch_optional(&self.pool) + .await + .map_err(db)?; + belegnummer.ok_or(ApplicationError::NotFound) + } + async fn load_erp_writeback( &self, delivery_id: Uuid, diff --git a/crates/infrastructure/src/storage/signature_storage.rs b/crates/infrastructure/src/storage/signature_storage.rs index 54abcc3..282b5ff 100644 --- a/crates/infrastructure/src/storage/signature_storage.rs +++ b/crates/infrastructure/src/storage/signature_storage.rs @@ -1,16 +1,17 @@ //! Lokaler Dateisystem-Adapter für `SignatureStorage`. //! //! Schreibt jede Unterschrift als PNG unter -//! `/_.png`. Der Pfad ist deterministisch: -//! ein Retry überschreibt dieselbe Datei, statt Müll anzuhäufen. Persistiert -//! wird in der DB nur die relative Referenz (Dateiname), nicht der absolute -//! Pfad — so bleibt ein Umzug des Verzeichnisses unkompliziert. +//! `/delivery__signature_.png` (z. B. +//! `delivery_V-30690287_signature_customer.png`). Der Pfad ist +//! deterministisch: ein Retry überschreibt dieselbe Datei, statt Müll +//! anzuhäufen. Persistiert wird in der DB nur die relative Referenz +//! (Dateiname), nicht der absolute Pfad — so bleibt ein Umzug des +//! Verzeichnisses unkompliziert. use std::path::PathBuf; use std::time::{Duration, SystemTime}; use async_trait::async_trait; -use uuid::Uuid; use holzleitner_application::error::ApplicationError; use holzleitner_application::ports::{SignatureRole, SignatureStorage}; @@ -28,8 +29,23 @@ impl LocalSignatureStorage { Ok(Self { base_dir }) } - fn file_name(delivery_id: Uuid, role: SignatureRole) -> String { - format!("{delivery_id}_{}.png", role.as_str()) + /// Baut den Dateinamen nach Schema + /// `delivery_{belegnummer}_signature_{role}.png`. Die Belegnummer wird + /// dateisystem-sicher gemacht (alles außer `A–Z a–z 0–9 . _ -` wird zu + /// `_`), damit ungewöhnliche ERP-Werte keinen Pfad-Ausbruch ermöglichen. + /// Übliche Belegnummern wie `V-30690287` bleiben dabei unverändert. + fn file_name(belegnummer: &str, role: SignatureRole) -> String { + let safe: String = belegnummer + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') { + c + } else { + '_' + } + }) + .collect(); + format!("delivery_{safe}_signature_{}.png", role.as_str()) } } @@ -37,11 +53,11 @@ impl LocalSignatureStorage { impl SignatureStorage for LocalSignatureStorage { async fn save( &self, - delivery_id: Uuid, + belegnummer: &str, role: SignatureRole, bytes: Vec, ) -> Result { - let name = Self::file_name(delivery_id, role); + let name = Self::file_name(belegnummer, role); let path = self.base_dir.join(&name); // Blocking-IO bewusst auf einen Blocking-Thread auslagern. tokio::task::spawn_blocking(move || std::fs::write(&path, &bytes)) @@ -73,31 +89,6 @@ impl SignatureStorage for LocalSignatureStorage { } } - async fn delete_for_delivery(&self, delivery_id: Uuid) -> Result<(), ApplicationError> { - // Beide deterministisch benannten Dateien (Kunde + Fahrer) entfernen. - let paths = [ - self.base_dir - .join(Self::file_name(delivery_id, SignatureRole::Customer)), - self.base_dir - .join(Self::file_name(delivery_id, SignatureRole::Driver)), - ]; - tokio::task::spawn_blocking(move || -> std::io::Result<()> { - for path in paths { - match std::fs::remove_file(&path) { - Ok(()) => {} - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => return Err(e), - } - } - Ok(()) - }) - .await - .map_err(|e| ApplicationError::Repository(format!("join error: {e}")))? - .map_err(|e| { - ApplicationError::Repository(format!("signatur löschen fehlgeschlagen: {e}")) - }) - } - async fn delete_older_than(&self, max_age: Duration) -> Result { let base = self.base_dir.clone(); tokio::task::spawn_blocking(move || -> std::io::Result {