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:
73
crates/application/src/dto/scan.rs
Normal file
73
crates/application/src/dto/scan.rs
Normal file
@ -0,0 +1,73 @@
|
||||
//! Bulk-Scan-Endpoint: Request, Response, pro-Event-Result.
|
||||
//!
|
||||
//! Idempotenz läuft über `client_scan_id`: ein UUID, das die App pro
|
||||
//! erzeugtem Scan-Event genau einmal vergibt. Retry desselben Events
|
||||
//! liefert `Duplicate` zurück. Pro Event ein eigener kleiner Vorgang —
|
||||
//! ein einzelner Reject blockiert die anderen nicht.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use holzleitner_domain::{AuditAction, ScanState};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApplyScansRequest {
|
||||
pub scans: Vec<ScanEvent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScanEvent {
|
||||
pub client_scan_id: Uuid,
|
||||
pub delivery_item_id: Uuid,
|
||||
pub action: AuditAction,
|
||||
/// Pflicht bei `Hold` und `Remove`. Sonst ignoriert.
|
||||
pub reason: Option<String>,
|
||||
pub client_scanned_at: DateTime<Utc>,
|
||||
/// Fahrzeug, in dem der Scan gemacht wurde. Muss zum
|
||||
/// angemeldeten Account gehören. `None` ist erlaubt, schwächt
|
||||
/// aber den Audit-Trail.
|
||||
pub actor_car_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ApplyScansResponse {
|
||||
pub results: Vec<ScanResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ScanResultStatus {
|
||||
/// Aktion wurde frisch angewendet; Audit-Eintrag geschrieben.
|
||||
Applied,
|
||||
/// `client_scan_id` war bereits bekannt. Item-State unverändert,
|
||||
/// `scan_state` zeigt den aktuellen Stand am Server.
|
||||
Duplicate,
|
||||
/// Aktion wurde abgelehnt (z. B. unbekanntes Item, invalider
|
||||
/// Statusübergang, fehlender Pflicht-Reason). `reason` füllt die
|
||||
/// Begründung; `scan_state` ist `None`.
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScanResult {
|
||||
pub client_scan_id: Uuid,
|
||||
pub status: ScanResultStatus,
|
||||
/// Bei `Rejected`: Begründung. Bei `Applied`/`Duplicate`: `None`.
|
||||
pub reason: Option<String>,
|
||||
/// Aktueller `scan_state` der Position nach der Verarbeitung —
|
||||
/// genau dann gesetzt, wenn der Server den Stand kennen konnte
|
||||
/// (`Applied` oder `Duplicate`). Erlaubt der App, die UI ohne
|
||||
/// Re-Fetch zu aktualisieren.
|
||||
pub delivery_item_id: Option<Uuid>,
|
||||
pub new_scan_state: Option<ScanState>,
|
||||
}
|
||||
Reference in New Issue
Block a user