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/usecases/create_delivery_note.rs
Normal file
73
crates/application/src/usecases/create_delivery_note.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use holzleitner_domain::DeliveryNote;
|
||||
|
||||
use crate::dto::CreateDeliveryNoteRequest;
|
||||
use crate::error::ApplicationError;
|
||||
use crate::ports::{CarRepository, DeliveryNoteRepository};
|
||||
|
||||
/// Legt eine neue Notiz an einer Lieferung an.
|
||||
///
|
||||
/// Validierung:
|
||||
/// * mindestens eines von `text` (nicht-leer nach trim) und
|
||||
/// `image_attachment` (nicht-leer nach trim) muss gesetzt sein.
|
||||
/// * `author_car_id` muss — falls gesetzt — zum angemeldeten Account gehören.
|
||||
pub struct CreateDeliveryNoteUseCase {
|
||||
repository: Arc<dyn DeliveryNoteRepository>,
|
||||
cars: Arc<dyn CarRepository>,
|
||||
}
|
||||
|
||||
impl CreateDeliveryNoteUseCase {
|
||||
pub fn new(
|
||||
repository: Arc<dyn DeliveryNoteRepository>,
|
||||
cars: Arc<dyn CarRepository>,
|
||||
) -> Self {
|
||||
Self { repository, cars }
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
delivery_id: Uuid,
|
||||
author_personalnummer: i64,
|
||||
request: CreateDeliveryNoteRequest,
|
||||
) -> Result<DeliveryNote, ApplicationError> {
|
||||
let text = clean(request.text);
|
||||
let image = clean(request.image_attachment);
|
||||
|
||||
if text.is_none() && image.is_none() {
|
||||
return Err(ApplicationError::Validation(
|
||||
"notiz braucht text oder image_attachment".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(car_id) = request.author_car_id {
|
||||
self.cars
|
||||
.assert_owned_by_account(&[car_id], author_personalnummer)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.repository
|
||||
.create(
|
||||
delivery_id,
|
||||
author_personalnummer,
|
||||
request.author_car_id,
|
||||
text,
|
||||
image,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim + leerer-String → None.
|
||||
fn clean(input: Option<String>) -> Option<String> {
|
||||
input.and_then(|s| {
|
||||
let trimmed = s.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_owned())
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user