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>
207 lines
8.1 KiB
Rust
207 lines
8.1 KiB
Rust
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,
|
||
}
|