Files
Holzleitner---Backend--aktu…/crates/application/src/usecases/complete_delivery.rs
Dennis Nemec 1e6dfb10b0 feat(signature): Dateinamen-Schema delivery_{Belegnummer}_signature_{role}.png
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>
2026-06-18 16:56:58 +02:00

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)
}
}