Files
Holzleitner---Backend--aktu…/crates/domain/src/delivery.rs
Dennis Nemec 6a9b5872e1 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>
2026-06-01 17:52:58 +02:00

207 lines
8.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::common::Address;
/// Lebenszyklus einer Lieferung.
///
/// `Held` ist für „heute nicht zustellbar, aber nicht endgültig abgesagt"
/// reserviert; `Canceled` ist endgültig. `Completed` setzt der
/// Abschluss-Flow am Ende der Auslieferung.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DeliveryState {
Active,
Held,
Canceled,
Completed,
}
/// Eine einzelne Lieferung an einen Kunden. Aggregat-Wurzel für die
/// Liefer-Items, Notizen und das ggf. zugeordnete Fahrzeug.
//
// Kein `Eq`-Derive, weil `prepaid_amount: f64` (Float kennt kein Eq —
// NaN-Verhalten). `PartialEq` reicht für unsere Vergleiche.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct Delivery {
pub id: Uuid,
pub tour_id: Uuid,
/// ERP-Beleg-Bezug: business-stabiles Paar `(Belegart, Belegnummer)`.
/// Überlebt den Belegkopf-Archivübergang.
pub erp_belegart_id: i64,
pub erp_belegnummer: String,
pub customer_id: Uuid,
/// Eingefrorene Liefer-Adresse zum Zeitpunkt des Tour-Syncs.
/// Schützt vor rückwirkenden Kunden-Adressänderungen.
pub delivery_address_snapshot: Address,
/// Fahrzeug-Zuordnung, gesetzt in der Auswählen-Phase.
/// Bei Ein-Auto-Teams beim Sync automatisch gefüllt.
pub assigned_car_id: Option<Uuid>,
/// Ausgewählte Ansprechpartner für genau diese Lieferung (Auswahl
/// aus `Customer.contacts`). Kann leer sein.
pub contact_person_ids: Vec<Uuid>,
/// Wunsch-Lieferzeit als Freitext (z. B. "vormittags", "ab 14:00").
pub desired_time: Option<String>,
/// Sondervereinbarungen (z. B. „Türklingel defekt, hintenrum klopfen").
pub special_agreements: Option<String>,
pub state: DeliveryState,
/// Begründung bei `state == Held` oder `state == Canceled`. Beim
/// Resume / Complete wieder `None`.
pub state_reason: Option<String>,
/// Bei Bestellung schon bezahlter Betrag in EUR. `0.0` wenn der
/// Kunde alles bei Lieferung zahlt. Wird vom ERP-Sync gefüllt.
pub prepaid_amount: f64,
/// Für den Restbetrag gewählte Zahlungsart — FK auf `payment_methods`.
/// Vom Kunden bei Bestellung festgelegt, der Fahrer übernimmt nur
/// die Abwicklung. Aktiv-Flag und Anzeige-Name werden über die
/// Stammdaten-Tabelle aufgelöst, nicht hier embeddet.
pub payment_method_id: Uuid,
}
/// Status einer einzelnen Scan-Position innerhalb eines Items.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ScanStatus {
InProgress,
Done,
Held,
Removed,
}
/// Eingebetteter Scan-Zustand pro [`DeliveryItem`]. Wird durch
/// `ScanAuditEntry`-Events fortgeschrieben — das Audit-Log ist die
/// Wahrheit über das WIE und WANN, dieses Embedded-VO ist die schnelle
/// Wahrheit über das WIEVIEL.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct ScanState {
pub scanned_quantity: i32,
/// Als Gutschrift entfernte Menge (0..=required_quantity). Eigene
/// Dimension neben `scanned_quantity`: „wie viele Stück dieser Zeile hat
/// der Kunde nicht angenommen". `status == Removed` entspricht
/// `credited_quantity == required_quantity` (ganze Zeile gutgeschrieben).
pub credited_quantity: i32,
pub status: ScanStatus,
/// Grund bei `status == Held` oder `status == Removed`.
pub held_reason: Option<String>,
pub last_updated_at: DateTime<Utc>,
}
/// Einzelposition einer Lieferung. Vereint reguläre Belegzeilen und
/// Stücklisten-Komponenten zu einer flachen Liste — die Stücklisten-
/// Hierarchie ist ein ERP-Konstrukt und wird beim Sync aufgelöst.
///
/// Über die Felder `belegzeilen_nr` und `komponenten_artikel_nr` bleibt
/// die ERP-Herkunft auflösbar.
// Kein `Eq`-Derive: `unit_price: f64` kennt kein `Eq`. `PartialEq` reicht.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeliveryItem {
pub id: Uuid,
pub delivery_id: Uuid,
pub article_id: Uuid,
pub required_quantity: i32,
pub warehouse_id: Uuid,
/// Stückpreis (brutto, EUR) aus dem ERP-Sync. Der Warenwert einer
/// Lieferung = Σ `unit_price` × ausgelieferte Menge.
pub unit_price: f64,
/// ERP-Belegzeilen-Nr (Position innerhalb des Belegs).
pub belegzeilen_nr: i32,
/// Bei Items aus einer Stückliste: Artikelnummer der Komponente.
/// Bei regulären Belegzeilen: `None`.
pub komponenten_artikel_nr: Option<String>,
/// Artikelnummer des Oberartikels, zu dem diese Komponente gehört.
/// `None` bei Oberartikeln/regulären Zeilen — die App rückt Komponenten
/// darüber unter ihrem Oberartikel ein.
pub parent_artikel_nr: Option<String>,
pub scan_state: ScanState,
}
impl DeliveryItem {
/// Tatsächlich auszuliefernde Menge = Soll minus Gutschrift. Nie negativ
/// (die Gutschrift ist per Constraint auf `required_quantity` gedeckelt).
pub fn delivered_quantity(&self) -> i32 {
(self.required_quantity - self.scan_state.credited_quantity).max(0)
}
/// Wert der ausgelieferten Menge dieser Position (brutto, EUR).
pub fn line_total(&self) -> f64 {
self.unit_price * self.delivered_quantity() as f64
}
}
/// Notiz an einer Lieferung — frei eingegeben durch den Fahrer.
///
/// Mindestens eines von `text` oder `image_attachment` muss gesetzt
/// sein. Die Constraint sitzt sowohl im DB-Schema (CHECK) als auch
/// in der Application-Schicht.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeliveryNote {
pub id: Uuid,
pub delivery_id: Uuid,
pub text: Option<String>,
/// Referenz auf einen Bild-Anhang (z. B. Object-Storage-Key/URL).
pub image_attachment: Option<String>,
/// Personalnummer des Akteurs (aus dem JWT). Pflicht.
pub author_personalnummer: i64,
/// Fahrzeug, falls bekannt — nullable bis das Backend Cars verwaltet.
pub author_car_id: Option<Uuid>,
/// Wenn die Notiz als Gutschrift-Grund zu einer Belegzeile angelegt
/// wurde: deren `DeliveryItem`-Id. Erlaubt dem Client, die Notiz beim
/// Zurücknehmen der Gutschrift (Unremove) gezielt wieder zu löschen.
/// `None` bei normalen Text-/Foto-Notizen.
pub credit_delivery_item_id: Option<Uuid>,
/// `true`, wenn die Notiz den Grund einer **Betrags-Gutschrift**
/// (Geld-Nachlass, Lieferungs-Ebene) dokumentiert. Erlaubt dem Client,
/// sie beim Entfernen der Gutschrift gezielt zu löschen.
pub is_amount_credit_note: bool,
/// `true`, wenn die lokale Bilddatei nach erfolgreichem Report-Upload
/// gelöscht wurde (das Bild steckt nun im Lieferbericht in DOCUframe).
/// Read-only; die App zeigt dann statt der Vorschau einen Hinweis.
/// Bei Text-Notizen / vorhandenem Bild: `false`.
#[serde(default)]
pub image_attachment_deleted: bool,
pub created_at: DateTime<Utc>,
}
/// Aktuelle Betrags-Gutschrift einer Lieferung (Geld-Nachlass, unabhängig von
/// Stückzahl). Abgeleitet aus dem jüngsten Ereignis im append-only
/// `delivery_credit_audit`; existiert nur, solange der letzte Stand `set`
/// (und nicht `remove`) ist. `delivery_id` macht den Eintrag — wie eine
/// Notiz — clientseitig per Lieferung join-bar.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeliveryCredit {
pub delivery_id: Uuid,
/// Gutschrift-Betrag in Cent (> 0, ≤ 15000).
pub amount_cents: i64,
pub reason: String,
}