use std::sync::Arc; use uuid::Uuid; use holzleitner_domain::Delivery; use crate::dto::CompleteDeliveryAcknowledgements; use crate::error::ApplicationError; use crate::ports::{ CarRepository, CompleteDeliveryInput, DeliveryCompletionRepository, SignatureRole, SignatureStorage, }; use crate::usecases::PushCompletionToErpUseCase; /// Schließt eine Lieferung ab: speichert beide Unterschriften lokal und /// schreibt — atomar im Repository — die Abschluss-Zeile + den Statuswechsel /// auf `completed`. /// /// Reihenfolge bewusst: erst die fachlichen Vor-Prüfungen ohne DB, dann die /// Dateien schreiben, dann das Repository (das die DB-abhängigen Gates unter /// Lock prüft). Schlägt das Repo-Gate fehl, bleiben höchstens die beiden /// deterministisch benannten PNG-Dateien liegen — ein erneuter Versuch /// überschreibt sie, es entsteht kein Müll. pub struct CompleteDeliveryUseCase { repository: Arc, signatures: Arc, cars: Arc, /// Optionales ERP-Rückschreiben. `None` ⇒ rein lokaler Abschluss /// (ERP_WRITEBACK_ENABLED=false / Dev / Seed-Daten ohne ERP-Beleg). erp_push: Option>, } impl CompleteDeliveryUseCase { pub fn new( repository: Arc, signatures: Arc, cars: Arc, erp_push: Option>, ) -> Self { Self { repository, signatures, cars, erp_push, } } pub async fn execute( &self, delivery_id: Uuid, author_personalnummer: i64, acknowledgements: CompleteDeliveryAcknowledgements, customer_signature_png: Vec, driver_signature_png: Vec, ) -> Result { // --- Vor-Prüfungen ohne DB ---------------------------------------- if !acknowledgements.receipt_confirmed { return Err(ApplicationError::Validation( "receipt must be confirmed before completion".into(), )); } if customer_signature_png.is_empty() { return Err(ApplicationError::Validation( "customer signature is required".into(), )); } if driver_signature_png.is_empty() { return Err(ApplicationError::Validation( "driver signature is required".into(), )); } if let Some(car_id) = acknowledgements.author_car_id { self.cars .assert_owned_by_account(&[car_id], author_personalnummer) .await?; } // --- 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(&belegnummer, SignatureRole::Customer, customer_signature_png) .await?; let driver_signature_path = self .signatures .save(&belegnummer, SignatureRole::Driver, driver_signature_png) .await?; // --- Atomarer Abschluss im Repository ----------------------------- let delivery = self .repository .complete(CompleteDeliveryInput { delivery_id, customer_signature_path, driver_signature_path, receipt_confirmed: acknowledgements.receipt_confirmed, notes_acknowledged: acknowledgements.notes_acknowledged, acknowledged_note_ids: acknowledgements.acknowledged_note_ids, payment_collected: acknowledgements.payment_collected, payment_method_id: acknowledgements.payment_method_id, completed_by_personalnummer: author_personalnummer, completed_by_car_id: acknowledgements.author_car_id, }) .await?; // --- ERP-Rückschreiben (optional, nach lokalem Commit) ------------ // Idempotent → ein Fehler hier lässt den lokalen Abschluss bestehen; // der Aufrufer bekommt den Fehler (502) und kann via Admin-Endpunkt // `POST /admin/push-completion` erneut pushen. if let Some(push) = &self.erp_push { push.execute(delivery_id).await?; } Ok(delivery) } }