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:
Dennis Nemec
2026-06-23 18:02:27 +02:00
parent 1e6dfb10b0
commit 2d364f3fb7
12 changed files with 503 additions and 32 deletions

View File

@ -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};

View 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>;
}

View File

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

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