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, /// Ausgewählte Ansprechpartner für genau diese Lieferung (Auswahl /// aus `Customer.contacts`). Kann leer sein. pub contact_person_ids: Vec, /// Wunsch-Lieferzeit als Freitext (z. B. "vormittags", "ab 14:00"). pub desired_time: Option, /// Sondervereinbarungen (z. B. „Türklingel defekt, hintenrum klopfen"). pub special_agreements: Option, pub state: DeliveryState, /// Begründung bei `state == Held` oder `state == Canceled`. Beim /// Resume / Complete wieder `None`. pub state_reason: Option, /// 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, pub last_updated_at: DateTime, } /// 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, /// 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, 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, /// Referenz auf einen Bild-Anhang (z. B. Object-Storage-Key/URL). pub image_attachment: Option, /// 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, /// 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, /// `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, } /// 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, }