Unterschrifts-Dateien folgen jetzt dem Schema
delivery_{Belegnummer}_signature_{customer|driver}.png
(z. B. delivery_V-30690287_signature_customer.png) statt zuvor
{delivery_id}_{role}.png.
- SignatureStorage::save nimmt Belegnummer statt delivery_id; Adapter
baut den Dateinamen + saubere Sanitisierung des Belegnummer-Anteils
(Schutz gegen Pfad-Ausbruch, übliche Werte wie V-30690287 bleiben).
- CompleteDeliveryUseCase löst die Belegnummer vor dem Speichern auf.
- Neue Lookup-Methode DeliveryCompletionRepository::belegnummer_for.
- Totes delete_for_delivery (Reconstruktion via delivery_id, keine
Aufrufer mehr seit Cron-Cleanup) entfernt.
Abwärtskompatibel: bestehende Signaturen werden über die in
delivery_completions gespeicherte Referenz geladen, alte Dateinamen
bleiben lesbar. Nur neue Abschlüsse verwenden das neue Schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
119 lines
4.5 KiB
Rust
119 lines
4.5 KiB
Rust
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<dyn DeliveryCompletionRepository>,
|
|
signatures: Arc<dyn SignatureStorage>,
|
|
cars: Arc<dyn CarRepository>,
|
|
/// Optionales ERP-Rückschreiben. `None` ⇒ rein lokaler Abschluss
|
|
/// (ERP_WRITEBACK_ENABLED=false / Dev / Seed-Daten ohne ERP-Beleg).
|
|
erp_push: Option<Arc<PushCompletionToErpUseCase>>,
|
|
}
|
|
|
|
impl CompleteDeliveryUseCase {
|
|
pub fn new(
|
|
repository: Arc<dyn DeliveryCompletionRepository>,
|
|
signatures: Arc<dyn SignatureStorage>,
|
|
cars: Arc<dyn CarRepository>,
|
|
erp_push: Option<Arc<PushCompletionToErpUseCase>>,
|
|
) -> Self {
|
|
Self {
|
|
repository,
|
|
signatures,
|
|
cars,
|
|
erp_push,
|
|
}
|
|
}
|
|
|
|
pub async fn execute(
|
|
&self,
|
|
delivery_id: Uuid,
|
|
author_personalnummer: i64,
|
|
acknowledgements: CompleteDeliveryAcknowledgements,
|
|
customer_signature_png: Vec<u8>,
|
|
driver_signature_png: Vec<u8>,
|
|
) -> Result<Delivery, ApplicationError> {
|
|
// --- 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)
|
|
}
|
|
}
|