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:
Dennis Nemec
2026-06-01 17:52:58 +02:00
parent 438040acce
commit 6a9b5872e1
137 changed files with 13700 additions and 218 deletions

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