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,85 @@
use std::sync::Arc;
use uuid::Uuid;
use holzleitner_domain::DeliveryCredit;
use crate::dto::{CreditAction, DeliveryCreditEventRequest};
use crate::error::ApplicationError;
use crate::ports::{CarRepository, DeliveryCreditRepository};
/// Obergrenze der Betrags-Gutschrift in Cent (150 €).
const MAX_CREDIT_CENTS: i64 = 15_000;
/// Wendet ein Gutschrift-Ereignis (`set`/`remove`) auf eine Lieferung an.
///
/// Validierung (fachlich, ohne DB):
/// * `Set`: Betrag Pflicht, `0 < amount ≤ 150 €` (beliebiger Betrag, keine
/// Schrittweite); Begründung Pflicht (nicht leer).
/// * `author_car_id` muss — falls gesetzt — zum Account gehören.
///
/// Den `active`-Check der Lieferung und die Idempotenz (`client_event_id`)
/// übernimmt das Repository mit der gelockten Zeile.
pub struct ApplyDeliveryCreditEventUseCase {
repository: Arc<dyn DeliveryCreditRepository>,
cars: Arc<dyn CarRepository>,
}
impl ApplyDeliveryCreditEventUseCase {
pub fn new(
repository: Arc<dyn DeliveryCreditRepository>,
cars: Arc<dyn CarRepository>,
) -> Self {
Self { repository, cars }
}
pub async fn execute(
&self,
delivery_id: Uuid,
author_personalnummer: i64,
request: DeliveryCreditEventRequest,
) -> Result<Option<DeliveryCredit>, ApplicationError> {
if let Some(car_id) = request.author_car_id {
self.cars
.assert_owned_by_account(&[car_id], author_personalnummer)
.await?;
}
let (amount_cents, reason) = match request.action {
CreditAction::Set => {
let amount = request.amount_cents.ok_or_else(|| {
ApplicationError::Validation("amount_cents required for set".into())
})?;
if amount <= 0 || amount > MAX_CREDIT_CENTS {
return Err(ApplicationError::Validation(format!(
"amount_cents must be in (0, {MAX_CREDIT_CENTS}]"
)));
}
let reason = request
.reason
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| {
ApplicationError::Validation("reason required for set".into())
})?
.to_owned();
(amount, Some(reason))
}
// Remove: Betrag/Grund irrelevant.
CreditAction::Remove => (0, None),
};
self.repository
.apply_event(
delivery_id,
request.client_event_id,
request.action,
amount_cents,
reason,
author_personalnummer,
request.author_car_id,
)
.await
}
}