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>
This commit is contained in:
115
crates/application/src/usecases/complete_delivery.rs
Normal file
115
crates/application/src/usecases/complete_delivery.rs
Normal file
@ -0,0 +1,115 @@
|
||||
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 -----------------------------------
|
||||
let customer_signature_path = self
|
||||
.signatures
|
||||
.save(delivery_id, SignatureRole::Customer, customer_signature_png)
|
||||
.await?;
|
||||
let driver_signature_path = self
|
||||
.signatures
|
||||
.save(delivery_id, 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user