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