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:
@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user