Files
Holzleitner---Backend--aktu…/crates/domain/src/audit.rs
Dennis Nemec 438040acce 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
2026-05-14 22:28:31 +02:00

68 lines
2.3 KiB
Rust

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>,
}