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:
@ -40,7 +40,8 @@ use holzleitner_application::usecases::{
|
||||
GetAttachmentPreviewUseCase, GetTourUseCase,
|
||||
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
|
||||
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
||||
ListServicesUseCase, MarkMailSentUseCase, PushCompletionToErpUseCase,
|
||||
ListPendingReviewsUseCase, ListServicesUseCase, MarkMailSentUseCase,
|
||||
PushCompletionToErpUseCase, ResolveReviewUseCase,
|
||||
SetDeliveryOrderUseCase, SetDeliveryServiceUseCase, SyncTourUseCase, UpdateDeliveryNoteUseCase,
|
||||
ProcessDeliveryReportUseCase, UpdateMyCarUseCase, UpdatePaymentMethodUseCase,
|
||||
UpdateServiceUseCase, UploadDeliveryNoteImageUseCase,
|
||||
@ -61,7 +62,8 @@ use holzleitner_infrastructure::report::{
|
||||
use holzleitner_infrastructure::persistence::{
|
||||
PgAccountRepository, PgAttachmentRepository, PgCarRepository, PgDeliveryCompletionRepository,
|
||||
PgDeliveryCreditRepository, PgDeliveryNoteRepository, PgDeliveryReportJobRepository,
|
||||
PgDeliveryRepository, PgDeliveryServiceRepository, PgPaymentMethodRepository, PgScanRepository,
|
||||
PgDeliveryRepository, PgDeliveryServiceRepository, PgPaymentMethodRepository, PgReviewRepository,
|
||||
PgScanRepository,
|
||||
PgServiceRepository, PgSyncRunRepository, PgTourRepository, PoolConfig, connect_and_migrate,
|
||||
};
|
||||
use holzleitner_infrastructure::storage::{LocalAttachmentStorage, LocalSignatureStorage};
|
||||
@ -253,7 +255,7 @@ pub(crate) async fn run_app(
|
||||
// Attachment-Upload/-Download über DOCUframe ist hier bewusst nicht mehr
|
||||
// verdrahtet (Code in `gsd::GsdService` bleibt für später erhalten).
|
||||
let gsd_service = Arc::new(GsdService::new(
|
||||
pool,
|
||||
pool.clone(),
|
||||
GsdConfig {
|
||||
rest_url: cfg.gsd.rest_url.clone(),
|
||||
app_key: cfg.gsd.app_key.clone(),
|
||||
@ -359,6 +361,11 @@ pub(crate) async fn run_app(
|
||||
let mark_mail_sent = Arc::new(MarkMailSentUseCase::new(
|
||||
delivery_completion_repository.clone(),
|
||||
));
|
||||
// Vier-Augen-Prüfung geänderter Lieferscheine.
|
||||
let review_repository = Arc::new(PgReviewRepository::new(pool.clone()));
|
||||
let list_pending_reviews =
|
||||
Arc::new(ListPendingReviewsUseCase::new(review_repository.clone()));
|
||||
let resolve_review = Arc::new(ResolveReviewUseCase::new(review_repository));
|
||||
let set_delivery_order = Arc::new(SetDeliveryOrderUseCase::new(tour_repository));
|
||||
let apply_scans = Arc::new(ApplyScansUseCase::new(
|
||||
scan_repository,
|
||||
@ -446,6 +453,8 @@ pub(crate) async fn run_app(
|
||||
push_completion_to_erp,
|
||||
list_delivered_belegnummern,
|
||||
mark_mail_sent,
|
||||
list_pending_reviews,
|
||||
resolve_review,
|
||||
apply_delivery_credit_event,
|
||||
create_delivery_note,
|
||||
update_delivery_note,
|
||||
|
||||
@ -55,6 +55,8 @@ use utoipa::openapi::security::{
|
||||
crate::routes::admin::push_completion,
|
||||
crate::routes::admin::delivered_belegnummern,
|
||||
crate::routes::admin::mark_mail_sent,
|
||||
crate::routes::admin::list_reviews,
|
||||
crate::routes::admin::resolve_review,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@ -125,6 +127,9 @@ use utoipa::openapi::security::{
|
||||
holzleitner_application::dto::DeliveryServiceResponse,
|
||||
holzleitner_application::usecases::ImportSummary,
|
||||
crate::routes::admin::DeliveredBelegnummernResponse,
|
||||
crate::routes::admin::PendingReviewResponse,
|
||||
crate::routes::admin::ReviewedItemResponse,
|
||||
crate::routes::admin::ResolveReviewRequest,
|
||||
crate::routes::admin::MarkMailSentRequest,
|
||||
crate::routes::admin::MarkMailSentResponse,
|
||||
crate::routes::tours::TourSummaryList,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
use axum::Json;
|
||||
use axum::Router;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::routing::{get, post};
|
||||
use chrono::NaiveDate;
|
||||
@ -30,6 +30,8 @@ pub fn router() -> Router<AppState> {
|
||||
get(delivered_belegnummern),
|
||||
)
|
||||
.route("/admin/mark-mail-sent", post(mark_mail_sent))
|
||||
.route("/admin/reviews", get(list_reviews))
|
||||
.route("/admin/reviews/{delivery_id}/resolve", post(resolve_review))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -222,3 +224,121 @@ pub async fn mark_mail_sent(
|
||||
tracing::info!(marked, "admin.mark_mail_sent.done");
|
||||
Ok(Json(MarkMailSentResponse { marked }))
|
||||
}
|
||||
|
||||
// ─── Vier-Augen-Prüfung geänderter Lieferscheine ────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ReviewedItemResponse {
|
||||
pub belegzeilen_nr: i32,
|
||||
pub artikel_nr: String,
|
||||
pub article_name: String,
|
||||
pub required_quantity: i32,
|
||||
pub credited_quantity: i32,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct PendingReviewResponse {
|
||||
pub delivery_id: String,
|
||||
pub erp_belegart_id: i64,
|
||||
pub erp_belegnummer: String,
|
||||
pub customer_name: String,
|
||||
/// Tourdatum (ISO `YYYY-MM-DD`).
|
||||
pub tour_date: String,
|
||||
/// Zeitpunkt der letzten beleg-ändernden Aktion (RFC 3339).
|
||||
pub last_change_at: String,
|
||||
/// Entfernte/teil-gutgeschriebene Positionen.
|
||||
pub credited_items: Vec<ReviewedItemResponse>,
|
||||
/// Geld-Gutschrift in Cent (0 = keine).
|
||||
pub money_credit_cents: i64,
|
||||
pub money_credit_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Listet alle geänderten Lieferscheine, die noch auf eine manuelle
|
||||
/// Bestätigung (Vier-Augen) warten — Entfernungen und/oder Geld-Gutschriften.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/admin/reviews",
|
||||
tag = "admin",
|
||||
responses(
|
||||
(status = 200, description = "Offene Prüfungen", body = [PendingReviewResponse]),
|
||||
(status = 401, description = "Admin-API-Key fehlt/ungültig")
|
||||
),
|
||||
security(("admin_api_key" = []))
|
||||
)]
|
||||
pub async fn list_reviews(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<PendingReviewResponse>>, ApiError> {
|
||||
let items = state.list_pending_reviews.execute().await?;
|
||||
tracing::info!(count = items.len(), "admin.reviews.list");
|
||||
let out = items
|
||||
.into_iter()
|
||||
.map(|r| PendingReviewResponse {
|
||||
delivery_id: r.delivery_id.to_string(),
|
||||
erp_belegart_id: r.erp_belegart_id,
|
||||
erp_belegnummer: r.erp_belegnummer,
|
||||
customer_name: r.customer_name,
|
||||
tour_date: r.tour_date.format("%Y-%m-%d").to_string(),
|
||||
last_change_at: r.last_change_at.to_rfc3339(),
|
||||
credited_items: r
|
||||
.credited_items
|
||||
.into_iter()
|
||||
.map(|i| ReviewedItemResponse {
|
||||
belegzeilen_nr: i.belegzeilen_nr,
|
||||
artikel_nr: i.artikel_nr,
|
||||
article_name: i.article_name,
|
||||
required_quantity: i.required_quantity,
|
||||
credited_quantity: i.credited_quantity,
|
||||
reason: i.reason,
|
||||
})
|
||||
.collect(),
|
||||
money_credit_cents: r.money_credit_cents,
|
||||
money_credit_reason: r.money_credit_reason,
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(out))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ResolveReviewRequest {
|
||||
/// Bearbeiter (Name/Kürzel), der die Prüfung bestätigt.
|
||||
pub resolved_by: String,
|
||||
/// Optionale Notiz zur getroffenen Entscheidung.
|
||||
#[serde(default)]
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
/// Bestätigt die Prüfung einer geänderten Lieferung (Vier-Augen) — die
|
||||
/// Lieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht
|
||||
/// erneut geändert).
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/admin/reviews/{delivery_id}/resolve",
|
||||
tag = "admin",
|
||||
params(("delivery_id" = String, Path, description = "UUID der Lieferung")),
|
||||
request_body = ResolveReviewRequest,
|
||||
responses(
|
||||
(status = 204, description = "Prüfung bestätigt"),
|
||||
(status = 400, description = "Ungültige delivery_id / Bearbeiter leer"),
|
||||
(status = 401, description = "Admin-API-Key fehlt/ungültig"),
|
||||
(status = 404, description = "Lieferung nicht gefunden")
|
||||
),
|
||||
security(("admin_api_key" = []))
|
||||
)]
|
||||
pub async fn resolve_review(
|
||||
State(state): State<AppState>,
|
||||
Path(delivery_id): Path<String>,
|
||||
Json(body): Json<ResolveReviewRequest>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
let id = Uuid::parse_str(delivery_id.trim()).map_err(|e| {
|
||||
ApiError(ApplicationError::Validation(format!(
|
||||
"ungültige delivery_id '{delivery_id}': {e}"
|
||||
)))
|
||||
})?;
|
||||
tracing::info!(%id, by = %body.resolved_by, "admin.reviews.resolve");
|
||||
state
|
||||
.resolve_review
|
||||
.execute(id, &body.resolved_by, body.note.as_deref())
|
||||
.await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@ use holzleitner_application::usecases::{
|
||||
GetAttachmentPreviewUseCase, GetTourUseCase,
|
||||
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
|
||||
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
||||
ListServicesUseCase, MarkMailSentUseCase, ProcessDeliveryReportUseCase,
|
||||
PushCompletionToErpUseCase,
|
||||
ListPendingReviewsUseCase, ListServicesUseCase, MarkMailSentUseCase,
|
||||
ProcessDeliveryReportUseCase, PushCompletionToErpUseCase, ResolveReviewUseCase,
|
||||
SetDeliveryOrderUseCase, SetDeliveryServiceUseCase, SyncTourUseCase, UpdateDeliveryNoteUseCase,
|
||||
UpdateMyCarUseCase, UpdatePaymentMethodUseCase, UpdateServiceUseCase,
|
||||
UploadDeliveryNoteImageUseCase,
|
||||
@ -50,6 +50,8 @@ pub struct AppState {
|
||||
pub list_delivered_belegnummern: Arc<ListDeliveredBelegnummernUseCase>,
|
||||
/// Admin: Liefermails von Belegnummern als versendet markieren (Dedup).
|
||||
pub mark_mail_sent: Arc<MarkMailSentUseCase>,
|
||||
pub list_pending_reviews: Arc<ListPendingReviewsUseCase>,
|
||||
pub resolve_review: Arc<ResolveReviewUseCase>,
|
||||
pub apply_delivery_credit_event: Arc<ApplyDeliveryCreditEventUseCase>,
|
||||
pub create_delivery_note: Arc<CreateDeliveryNoteUseCase>,
|
||||
pub update_delivery_note: Arc<UpdateDeliveryNoteUseCase>,
|
||||
|
||||
Reference in New Issue
Block a user