feat(review): Vier-Augen-Prüfung für geänderte Lieferscheine
Geänderte Lieferscheine (Artikel entfernt/teil-gutgeschrieben ODER
Geld-Gutschrift) sollen von der Fakturierung gegengeprüft werden, statt
die Menge im ERP zu reduzieren (was den Lagerbestand inkonsistent machte,
weil der Raw-Writeback die ERPframe-Engine umgeht).
- ERP-Writeback: kein Setzen der Belegzeilen-Menge mehr → ERP-Beleg bleibt
im Original (kein Bestandskonflikt). Geld-Gutschrift wird weiter
geschrieben (unkritisch, nur Vier-Augen). delivered-at/State/Zahlbed
bleiben.
- Migration 0031: review_resolved_at/by/note auf deliveries. Der Status
wird ABGELEITET (Abweichung via credited_quantity / aktueller Gutschrift
vs. resolved_at), daher: Originalzustand wiederhergestellt ⇒ Flag weg;
nach Bestätigung erneut geändert ⇒ wieder offen.
- ReviewRepository (Port + Pg-Impl) + Use Cases ListPendingReviews/
ResolveReview.
- Endpoints: GET /admin/reviews (offene Prüfungen inkl. Änderungsdetails
aus scan_audit/credit_audit) + POST /admin/reviews/{delivery_id}/resolve
(Admin-Key).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,7 @@ pub mod delivery_service_repository;
|
||||
pub mod docuframe_report_gateway;
|
||||
pub mod driver_identity_provisioner;
|
||||
pub mod payment_method_repository;
|
||||
pub mod review_repository;
|
||||
pub mod delivery_completion_repository;
|
||||
pub mod erp_delivery_source;
|
||||
pub mod erp_delivery_writeback;
|
||||
@ -57,6 +58,7 @@ pub use erp_delivery_writeback::{
|
||||
ErpDeliveryWriteback, ErpFinishDeliveryCommand, ErpLineQuantity,
|
||||
};
|
||||
pub use payment_method_repository::PaymentMethodRepository;
|
||||
pub use review_repository::{PendingReview, ReviewRepository, ReviewedItem};
|
||||
pub use scan_repository::{ApplyScanOutcome, ScanRepository};
|
||||
pub use service_repository::ServiceRepository;
|
||||
pub use signature_storage::{SignatureRole, SignatureStorage};
|
||||
|
||||
61
crates/application/src/ports/review_repository.rs
Normal file
61
crates/application/src/ports/review_repository.rs
Normal file
@ -0,0 +1,61 @@
|
||||
//! Port für die Vier-Augen-Prüfung geänderter Lieferscheine.
|
||||
//!
|
||||
//! Eine Lieferung gilt als „prüfbedürftig", sobald sie vom ursprünglichen
|
||||
//! ERP-Beleg abweicht (Artikel entfernt/teil-gutgeschrieben ODER Geld-
|
||||
//! Gutschrift). Der Status wird **abgeleitet** (siehe Migration 0031); hier
|
||||
//! kommen nur die Lese-Liste der offenen Prüfungen und das manuelle Auflösen
|
||||
//! an.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::ApplicationError;
|
||||
|
||||
/// Eine vom Original abweichende Position (entfernt/teil-gutgeschrieben).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReviewedItem {
|
||||
pub belegzeilen_nr: i32,
|
||||
pub artikel_nr: String,
|
||||
pub article_name: String,
|
||||
pub required_quantity: i32,
|
||||
pub credited_quantity: i32,
|
||||
/// Jüngster Entfern-Grund aus dem Audit (falls vorhanden).
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Eine offene (noch nicht bestätigte) Prüfung eines geänderten Lieferscheins.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PendingReview {
|
||||
pub delivery_id: Uuid,
|
||||
pub erp_belegart_id: i64,
|
||||
pub erp_belegnummer: String,
|
||||
pub customer_name: String,
|
||||
pub tour_date: NaiveDate,
|
||||
/// Zeitpunkt der letzten beleg-ändernden Aktion (Entfernen/Gutschrift).
|
||||
pub last_change_at: DateTime<Utc>,
|
||||
/// Entfernte/teil-gutgeschriebene Positionen.
|
||||
pub credited_items: Vec<ReviewedItem>,
|
||||
/// Geld-Gutschrift in Cent (0 = keine).
|
||||
pub money_credit_cents: i64,
|
||||
pub money_credit_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ReviewRepository: Send + Sync {
|
||||
/// Alle Lieferungen, die vom Original abweichen UND seit der letzten
|
||||
/// Änderung noch nicht bestätigt wurden (= Status `pending`). Sortiert
|
||||
/// nach jüngster Änderung zuerst.
|
||||
async fn list_pending(&self) -> Result<Vec<PendingReview>, ApplicationError>;
|
||||
|
||||
/// Markiert die Prüfung einer Lieferung als erledigt (Vier-Augen):
|
||||
/// speichert Zeitpunkt + Bearbeiter + optionale Notiz. Idempotent
|
||||
/// (erneutes Auflösen überschreibt). `NotFound`, wenn die Lieferung
|
||||
/// nicht existiert.
|
||||
async fn resolve(
|
||||
&self,
|
||||
delivery_id: Uuid,
|
||||
resolved_by: &str,
|
||||
note: Option<&str>,
|
||||
) -> Result<(), ApplicationError>;
|
||||
}
|
||||
@ -24,6 +24,7 @@ pub mod mark_mail_sent;
|
||||
pub mod payment_methods;
|
||||
pub mod process_delivery_report;
|
||||
pub mod push_completion_to_erp;
|
||||
pub mod reviews;
|
||||
pub mod services;
|
||||
pub mod set_delivery_order;
|
||||
pub mod sync_tour;
|
||||
@ -54,6 +55,7 @@ pub use payment_methods::{
|
||||
};
|
||||
pub use process_delivery_report::ProcessDeliveryReportUseCase;
|
||||
pub use push_completion_to_erp::PushCompletionToErpUseCase;
|
||||
pub use reviews::{ListPendingReviewsUseCase, ResolveReviewUseCase};
|
||||
pub use services::{
|
||||
CreateServiceUseCase, DeleteDeliveryServiceUseCase, DeleteServiceUseCase,
|
||||
ListServicesUseCase, SetDeliveryServiceUseCase, UpdateServiceUseCase,
|
||||
|
||||
54
crates/application/src/usecases/reviews.rs
Normal file
54
crates/application/src/usecases/reviews.rs
Normal file
@ -0,0 +1,54 @@
|
||||
//! Use Cases für die Vier-Augen-Prüfung geänderter Lieferscheine.
|
||||
//!
|
||||
//! Dünne Hüllen über [`ReviewRepository`]: die Fakturierung listet offene
|
||||
//! Prüfungen und löst sie nach manueller Entscheidung auf.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::ApplicationError;
|
||||
use crate::ports::{PendingReview, ReviewRepository};
|
||||
|
||||
/// Listet alle offenen (noch nicht bestätigten) Prüfungen geänderter
|
||||
/// Lieferscheine.
|
||||
pub struct ListPendingReviewsUseCase {
|
||||
repository: Arc<dyn ReviewRepository>,
|
||||
}
|
||||
|
||||
impl ListPendingReviewsUseCase {
|
||||
pub fn new(repository: Arc<dyn ReviewRepository>) -> Self {
|
||||
Self { repository }
|
||||
}
|
||||
|
||||
pub async fn execute(&self) -> Result<Vec<PendingReview>, ApplicationError> {
|
||||
self.repository.list_pending().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Löst die Prüfung einer Lieferung auf (Vier-Augen-Bestätigung).
|
||||
pub struct ResolveReviewUseCase {
|
||||
repository: Arc<dyn ReviewRepository>,
|
||||
}
|
||||
|
||||
impl ResolveReviewUseCase {
|
||||
pub fn new(repository: Arc<dyn ReviewRepository>) -> Self {
|
||||
Self { repository }
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
delivery_id: Uuid,
|
||||
resolved_by: &str,
|
||||
note: Option<&str>,
|
||||
) -> Result<(), ApplicationError> {
|
||||
let by = resolved_by.trim();
|
||||
if by.is_empty() {
|
||||
return Err(ApplicationError::Validation(
|
||||
"resolved_by darf nicht leer sein".into(),
|
||||
));
|
||||
}
|
||||
let note = note.map(str::trim).filter(|s| !s.is_empty());
|
||||
self.repository.resolve(delivery_id, by, note).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user