Initial: Rust-Backend mit Clean Architecture (domain/application/infrastructure/api)
Vier-Crate-Workspace mit:
- Domain: Account, Car, Tour, Delivery, DeliveryItem, DeliveryNote, Customer,
Article, Warehouse, ScanState, AuditAction — alle mit serde + feature-gated
utoipa::ToSchema.
- Application: Ports (TourRepository, DeliveryRepository, ScanRepository,
DeliveryNoteRepository, CarRepository, AuthService) und Use Cases.
- Infrastructure: Postgres-Adapter via sqlx (PgTourRepository etc.) +
Keycloak-AuthService mit JWKS-Cache + OIDC-Discovery.
- API: Axum 0.8, utoipa-OpenAPI + Swagger-UI, JWT-Bearer-Middleware,
AuthenticatedUser-Extractor.
Endpoints:
- GET /me/tours/today, /tours/{id}, /accounts/{pn}, /me/cars, /health
- POST /sync/tour, /scans (bulk + idempotent via clientScanId),
/deliveries/{id}/{hold,resume,cancel,complete,notes}, /me/cars
- PUT /tours/{id}/delivery-order, /deliveries/{id}/assigned-car, /me/cars/{id}
- PATCH /me/cars/{id}
Datenmodell:
- 6 Migrationen (accounts, tours/deliveries/items + Stammdaten,
scan_audit mit clientScanId-UNIQUE, state_reason refactor,
delivery_notes, cars + FKs nachziehen).
- Business-stabile Beleg-Keys (belegart_id, belegnummer) für ERP-Sync.
- Append-only scan_audit + embedded scan_state als doppelte Wahrheit.
Dev-Setup:
- docker-compose mit Postgres 17 + Keycloak 26
- Keycloak-Realm 'holzleitner' mit Public-Client (PKCE), Testfahrer
(PN 1001) + Audience-/Personalnummer-Mapper
This commit is contained in:
18
crates/domain/Cargo.toml
Normal file
18
crates/domain/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "holzleitner-domain"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[features]
|
||||
# Opt-in: schaltet `utoipa::ToSchema`-Ableitungen für die OpenAPI-
|
||||
# Generierung in der API-Schicht frei. Konsumenten ohne OpenAPI-Bedarf
|
||||
# (z. B. CLI-Tools, Tests) bleiben utoipa-frei.
|
||||
openapi = ["dep:utoipa"]
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
utoipa = { workspace = true, optional = true }
|
||||
19
crates/domain/src/account.rs
Normal file
19
crates/domain/src/account.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Account eines Liefer-Unternehmens oder Einzel-Lieferfahrers.
|
||||
///
|
||||
/// Die Personalnummer ist sowohl Primärschlüssel als auch Login-ID. Sie
|
||||
/// stammt aus dem ERP-Stamm — entweder ein Unternehmen (juristische
|
||||
/// Person, eigener Personalnummern-Kreis) oder eine natürliche Person.
|
||||
///
|
||||
/// Mehrere physische Fahrer können denselben Account benutzen; das Modell
|
||||
/// unterscheidet sie nicht, sondern loggt die Aktivität auf [`crate::domain::Car`]-
|
||||
/// Ebene (siehe Audit-Log).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Account {
|
||||
pub personalnummer: i64,
|
||||
pub name: String,
|
||||
pub active: bool,
|
||||
}
|
||||
19
crates/domain/src/article.rs
Normal file
19
crates/domain/src/article.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Artikel. ERP-Mirror; die `article_number` ist die business-stabile
|
||||
/// Artikelnummer aus dem ERP-Stamm und dient gleichzeitig als Brücke.
|
||||
///
|
||||
/// `scannable = false` markiert nicht-physische Positionen wie
|
||||
/// Dienstleistungen, Versandpauschalen o.ä. — sie tauchen zwar als
|
||||
/// `DeliveryItem` auf, blockieren aber den Beladen-Fortschritt nicht.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Article {
|
||||
pub id: Uuid,
|
||||
pub article_number: String,
|
||||
pub name: String,
|
||||
pub scannable: bool,
|
||||
pub default_warehouse_id: Option<Uuid>,
|
||||
}
|
||||
67
crates/domain/src/audit.rs
Normal file
67
crates/domain/src/audit.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::delivery::ScanStatus;
|
||||
|
||||
/// Aktion-Typen im Scan-Audit-Log.
|
||||
///
|
||||
/// * `Scan` / `Unscan` verändern die `scanned_quantity` (+1 / -1).
|
||||
/// * `Hold` / `Unhold` ändern nur den Status, keine Menge.
|
||||
/// * `Remove` markiert die Position als entfernt (Status `Removed`,
|
||||
/// z. B. weil der Kunde sie nicht annimmt).
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditAction {
|
||||
Scan,
|
||||
Unscan,
|
||||
Hold,
|
||||
Unhold,
|
||||
Remove,
|
||||
}
|
||||
|
||||
/// Append-only Audit-Log-Eintrag: jedes Ereignis am Scan-Zustand einer
|
||||
/// Position bekommt eine eigene Zeile. Nie geupdated, nie gelöscht.
|
||||
///
|
||||
/// Beleg-Bezug wird denormalisiert mitgeführt, damit der Audit-Trail
|
||||
/// auch dann auflösbar bleibt, wenn das zugehörige `DeliveryItem`
|
||||
/// irgendwann archiviert oder bereinigt wird.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScanAuditEntry {
|
||||
pub id: Uuid,
|
||||
/// Vom Client vergebene UUID — Idempotenz-Schlüssel beim Retry.
|
||||
pub client_scan_id: Uuid,
|
||||
pub delivery_item_id: Uuid,
|
||||
|
||||
pub action: AuditAction,
|
||||
|
||||
/// Signed Δ in `scanned_quantity`: +1 bei SCAN, -1 bei UNSCAN, 0 sonst.
|
||||
pub delta: i32,
|
||||
|
||||
/// Snapshot der `scanned_quantity` direkt NACH dieser Aktion.
|
||||
/// Vermeidet teure Aggregat-Queries bei Reports.
|
||||
pub resulting_quantity: i32,
|
||||
|
||||
/// Status der Position NACH dieser Aktion.
|
||||
pub resulting_status: ScanStatus,
|
||||
|
||||
/// Grund bei HOLD / REMOVE (jeweils Pflicht).
|
||||
pub reason: Option<String>,
|
||||
|
||||
/// Akteur — Personalnummer aus dem JWT.
|
||||
pub actor_personalnummer: i64,
|
||||
/// Akteur-Fahrzeug, sofern bekannt (cars werden später verwaltet).
|
||||
pub actor_car_id: Option<Uuid>,
|
||||
|
||||
pub client_scanned_at: DateTime<Utc>,
|
||||
pub server_recorded_at: DateTime<Utc>,
|
||||
|
||||
// ── Denormalisierter ERP-Beleg-Bezug (Archiv-stabil) ───────────────
|
||||
pub erp_belegart_id: i64,
|
||||
pub erp_belegnummer: String,
|
||||
pub erp_belegzeilen_nr: i32,
|
||||
pub erp_komponenten_artikel_nr: Option<String>,
|
||||
}
|
||||
19
crates/domain/src/car.rs
Normal file
19
crates/domain/src/car.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Fahrzeug eines [`crate::domain::Account`]. Wird in der App selbst
|
||||
/// gepflegt — kein ERP-Spiegel. Eindeutig per UUID.
|
||||
///
|
||||
/// Im Audit-Log ist der `Car` der „Akteur": die Personalnummer-Ebene
|
||||
/// (Account) ist gröber und unterscheidet nicht zwischen mehreren
|
||||
/// gleichzeitig aktiven Fahrern desselben Subunternehmens.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Car {
|
||||
pub id: Uuid,
|
||||
/// Verweis auf [`crate::domain::Account::personalnummer`].
|
||||
pub account_id: i64,
|
||||
pub plate: String,
|
||||
pub active: bool,
|
||||
}
|
||||
20
crates/domain/src/common.rs
Normal file
20
crates/domain/src/common.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Postanschrift — wird sowohl als aktuelle Kundenanschrift in [`Customer`]
|
||||
/// als auch als unveränderlicher Snapshot in [`crate::domain::Delivery`]
|
||||
/// verwendet (`delivery_address_snapshot`).
|
||||
///
|
||||
/// Bewusst als Value Object modelliert: gleiche Adresse = gleicher Wert.
|
||||
/// Strikte Equality erleichtert Sync-Diffs zwischen ERP und Backend.
|
||||
///
|
||||
/// [`Customer`]: crate::domain::Customer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Address {
|
||||
pub street: String,
|
||||
pub house_number: String,
|
||||
pub postal_code: String,
|
||||
pub city: String,
|
||||
pub country: String,
|
||||
}
|
||||
37
crates/domain/src/customer.rs
Normal file
37
crates/domain/src/customer.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::common::Address;
|
||||
|
||||
/// Kunde. ERP-Mirror: die Stammdaten gehören dem ERP, wir spiegeln sie
|
||||
/// für die App. Die `erp_customer_id` ist die Brücke zurück (in der
|
||||
/// Regel die `Kunde.row_id` aus ERPframe).
|
||||
///
|
||||
/// Die `Customer.address` ist die *aktuelle* Anschrift. Für historische
|
||||
/// Stabilität führt [`crate::domain::Delivery`] zusätzlich einen
|
||||
/// `delivery_address_snapshot` — Adress-Änderungen wirken nicht
|
||||
/// rückwirkend auf bereits zugestellte oder geplante Lieferungen.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Customer {
|
||||
pub id: Uuid,
|
||||
pub erp_customer_id: i64,
|
||||
pub name: String,
|
||||
pub address: Address,
|
||||
}
|
||||
|
||||
/// Ansprechpartner eines Kunden. Ein Kunde kann mehrere Kontaktpersonen
|
||||
/// haben (z. B. Empfang vor Ort + Geschäftsführung). Eine Lieferung wählt
|
||||
/// 0..N davon als aktive Kontakte aus (siehe
|
||||
/// `Delivery::contact_person_ids`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomerContact {
|
||||
pub id: Uuid,
|
||||
pub customer_id: Uuid,
|
||||
pub name: String,
|
||||
pub phone: Option<String>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
135
crates/domain/src/delivery.rs
Normal file
135
crates/domain/src/delivery.rs
Normal file
@ -0,0 +1,135 @@
|
||||
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.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[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>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
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.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[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,
|
||||
|
||||
/// 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>,
|
||||
|
||||
pub scan_state: ScanState,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
38
crates/domain/src/lib.rs
Normal file
38
crates/domain/src/lib.rs
Normal file
@ -0,0 +1,38 @@
|
||||
//! Domänenmodell des Lieferservice-Backends.
|
||||
//!
|
||||
//! Aufbau pro Aggregat-Wurzel bzw. zusammengehöriger Begriffsgruppe.
|
||||
//! Reihenfolge der Module entspricht inhaltlich der Datenfluss-Logik:
|
||||
//! Stamm- und Strukturdaten zuerst, danach transaktionale Aggregate
|
||||
//! (Tour → Delivery → Audit), zuletzt der prozessuale Zustand.
|
||||
//!
|
||||
//! **Konventionen**
|
||||
//! * Rust-Felder in `snake_case`, JSON via `serde` in `camelCase`.
|
||||
//! * Enum-Varianten in `PascalCase`, JSON via `serde` in `snake_case`.
|
||||
//! * Identifier vom ERP (Personalnummer, Belegart/-nummer, Artikelnummer)
|
||||
//! behalten ihre fachlichen Namen, weil sie als Brücken im Datenmodell
|
||||
//! erkennbar bleiben sollen.
|
||||
//! * Eigene IDs sind UUIDs — entkoppelt vom ERP, generieren wir selbst.
|
||||
|
||||
#![allow(dead_code)] // Modelle werden später von Service-Schicht genutzt.
|
||||
|
||||
mod account;
|
||||
mod article;
|
||||
mod audit;
|
||||
mod car;
|
||||
mod common;
|
||||
mod customer;
|
||||
mod delivery;
|
||||
mod process_state;
|
||||
mod tour;
|
||||
mod warehouse;
|
||||
|
||||
pub use account::Account;
|
||||
pub use article::Article;
|
||||
pub use audit::{AuditAction, ScanAuditEntry};
|
||||
pub use car::Car;
|
||||
pub use common::Address;
|
||||
pub use customer::{Customer, CustomerContact};
|
||||
pub use delivery::{Delivery, DeliveryItem, DeliveryNote, DeliveryState, ScanState, ScanStatus};
|
||||
pub use process_state::{DeliveryPhase, DeliveryProcessState};
|
||||
pub use tour::Tour;
|
||||
pub use warehouse::Warehouse;
|
||||
50
crates/domain/src/process_state.rs
Normal file
50
crates/domain/src/process_state.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Phasen des geführten Tagesablaufs eines Fahrers.
|
||||
///
|
||||
/// Die Reihenfolge der Varianten entspricht dem natürlichen Fortschritt;
|
||||
/// `PartialOrd`/`Ord` werden derived, sodass `current < max_reached`
|
||||
/// als „besucht"-Check funktioniert.
|
||||
///
|
||||
/// `Auswaehlen` ist optional und nur sichtbar bei Accounts mit mehreren
|
||||
/// Fahrzeugen — die Eintrittsphase bei einem Ein-Auto-Account ist
|
||||
/// `Sortieren`.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord,
|
||||
)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DeliveryPhase {
|
||||
Auswaehlen,
|
||||
Sortieren,
|
||||
Beladen,
|
||||
Ausliefern,
|
||||
}
|
||||
|
||||
/// Prozess-State pro Fahrzeug pro Tag. Trägt sowohl die aktuelle Phase
|
||||
/// als auch die höchste je erreichte Phase — damit Rück- und
|
||||
/// Vorwärtssprünge zwischen besuchten Phasen erlaubt sind.
|
||||
///
|
||||
/// `sorting_order` ist die in der Sortier-Phase festgelegte
|
||||
/// Auslieferungsreihenfolge (Liste von [`crate::domain::Delivery`]-IDs).
|
||||
/// Bei der Beladung wird sie umgekehrt benutzt (letzte Lieferung zuerst
|
||||
/// ins Auto).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeliveryProcessState {
|
||||
pub id: Uuid,
|
||||
pub car_id: Uuid,
|
||||
pub date: NaiveDate,
|
||||
|
||||
pub current_phase: DeliveryPhase,
|
||||
pub max_reached_phase: DeliveryPhase,
|
||||
|
||||
pub sorting_order: Vec<Uuid>,
|
||||
|
||||
/// Gesetzt sobald die Sortierung bestätigt wurde — markiert den
|
||||
/// Übergang von `Sortieren` nach `Beladen` als „offiziell".
|
||||
pub confirmed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
20
crates/domain/src/tour.rs
Normal file
20
crates/domain/src/tour.rs
Normal file
@ -0,0 +1,20 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Tour eines Tages, pro [`crate::domain::Account`]. Aggregat-Wurzel
|
||||
/// für die Lieferungen dieses Tages — die einzelnen [`crate::domain::Delivery`]
|
||||
/// referenzieren ihre Tour per FK.
|
||||
///
|
||||
/// Der Sync vom ERP läuft in der Regel einmal am Vortag und füllt eine
|
||||
/// neue Tour-Zeile inklusive Delivery- und DeliveryItem-Strukturen.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tour {
|
||||
pub id: Uuid,
|
||||
pub account_id: i64,
|
||||
pub date: NaiveDate,
|
||||
/// Zeitpunkt des letzten ERP-Sync — für Drift-Erkennung.
|
||||
pub synced_at: DateTime<Utc>,
|
||||
}
|
||||
15
crates/domain/src/warehouse.rs
Normal file
15
crates/domain/src/warehouse.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Lager. ERP-Mirror; `code` ist die ERP-Lager-Nr (z. B. `"0"` für das
|
||||
/// Standardlager). Das `is_standard`-Flag ist der schnelle Filter für
|
||||
/// die Beladen-Logik („nur Standardlager-Artikel zählen für Fertig").
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Warehouse {
|
||||
pub id: Uuid,
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub is_standard: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user