feat(dev): /dev/reset-delivery — einzelne Lieferung per Belegnummer zurücksetzen
Setzt EINE Lieferung zurück, ohne die Tour zu löschen (Alternative zum destruktiven /dev/resync): Abschluss (delivery_completions), Scan-/ Gutschrift-Audit + offener Report-Job weg, Positionen zurück (scanned/ credited = 0, Status in_progress), Lieferung wieder active (state_reason/ assigned_car/review_* geleert). Notizen/Dienstleistungen/Anhänge bleiben. Vervollständigt den bisher nur als Port-Stub vorhandenen reset_delivery_by_belegnummer (Build war dadurch kaputt) + Use Case + DEV-Route (nur bei dev.sync_enabled gemountet, unauthentifiziert). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -36,7 +36,8 @@ use holzleitner_application::usecases::{
|
|||||||
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
|
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
|
||||||
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
|
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
|
||||||
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
||||||
DeleteServiceUseCase, DevResyncToursUseCase, GenerateDeliveryReportUseCase, GetAccountUseCase,
|
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
|
||||||
|
GenerateDeliveryReportUseCase, GetAccountUseCase,
|
||||||
GetAttachmentPreviewUseCase, GetTourUseCase,
|
GetAttachmentPreviewUseCase, GetTourUseCase,
|
||||||
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
|
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
|
||||||
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
||||||
@ -345,6 +346,9 @@ pub(crate) async fn run_app(
|
|||||||
tour_repository.clone(),
|
tour_repository.clone(),
|
||||||
import_erp_tours.clone(),
|
import_erp_tours.clone(),
|
||||||
));
|
));
|
||||||
|
// DEV-ONLY: einzelne Lieferung zurücksetzen (per Belegnummer).
|
||||||
|
let dev_reset_delivery =
|
||||||
|
Arc::new(DevResetDeliveryUseCase::new(tour_repository.clone()));
|
||||||
// ERP-Rückschreiben beim Lieferabschluss. Der Push-Use-Case wird IMMER
|
// ERP-Rückschreiben beim Lieferabschluss. Der Push-Use-Case wird IMMER
|
||||||
// gebaut (Admin-Retry-Endpunkt nutzt ihn manuell). Ob der normale
|
// gebaut (Admin-Retry-Endpunkt nutzt ihn manuell). Ob der normale
|
||||||
// Abschluss-Pfad automatisch pusht, steuert `ERP_WRITEBACK_ENABLED`.
|
// Abschluss-Pfad automatisch pusht, steuert `ERP_WRITEBACK_ENABLED`.
|
||||||
@ -447,6 +451,7 @@ pub(crate) async fn run_app(
|
|||||||
set_delivery_order,
|
set_delivery_order,
|
||||||
import_erp_tours: import_erp_tours.clone(),
|
import_erp_tours: import_erp_tours.clone(),
|
||||||
dev_resync_tours,
|
dev_resync_tours,
|
||||||
|
dev_reset_delivery,
|
||||||
generate_delivery_report,
|
generate_delivery_report,
|
||||||
process_delivery_report: process_delivery_report.clone(),
|
process_delivery_report: process_delivery_report.clone(),
|
||||||
report_upload_enabled: cfg.report.upload_enabled,
|
report_upload_enabled: cfg.report.upload_enabled,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ use crate::state::AppState;
|
|||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/dev/resync", post(dev_resync))
|
.route("/dev/resync", post(dev_resync))
|
||||||
|
.route("/dev/reset-delivery", post(dev_reset_delivery))
|
||||||
.route("/dev/generate-report", post(dev_generate_report))
|
.route("/dev/generate-report", post(dev_generate_report))
|
||||||
.route("/dev/process-report", post(dev_process_report))
|
.route("/dev/process-report", post(dev_process_report))
|
||||||
.route("/dev/unmark-mail-sent", post(dev_unmark_mail_sent))
|
.route("/dev/unmark-mail-sent", post(dev_unmark_mail_sent))
|
||||||
@ -64,6 +65,43 @@ pub async fn dev_resync(
|
|||||||
Ok(Json(summary))
|
Ok(Json(summary))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DevResetDeliveryQuery {
|
||||||
|
/// ERP-Belegnummer der zurückzusetzenden Lieferung.
|
||||||
|
pub belegnummer: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DevResetDeliveryResponse {
|
||||||
|
pub belegnummer: String,
|
||||||
|
/// Anzahl zurückgesetzter Lieferungen (1 bei Erfolg).
|
||||||
|
pub reset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DEV-ONLY, UNAUTHENTIFIZIERT: setzt EINE Lieferung (per Belegnummer) zurück —
|
||||||
|
/// Abschluss, Scan-/Gutschrift-Audit + Report-Job weg, Positionen + Lieferung
|
||||||
|
/// zurück auf `active`. Notizen/Dienstleistungen/Anhänge bleiben. 404, wenn die
|
||||||
|
/// Belegnummer unbekannt ist.
|
||||||
|
pub async fn dev_reset_delivery(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(query): Query<DevResetDeliveryQuery>,
|
||||||
|
) -> Result<Json<DevResetDeliveryResponse>, ApiError> {
|
||||||
|
tracing::warn!(belegnummer = %query.belegnummer, "dev.reset_delivery");
|
||||||
|
let reset = state.dev_reset_delivery.execute(&query.belegnummer).await?;
|
||||||
|
if reset == 0 {
|
||||||
|
return Err(ApiError(ApplicationError::NotFound));
|
||||||
|
}
|
||||||
|
tracing::info!(
|
||||||
|
belegnummer = %query.belegnummer,
|
||||||
|
reset,
|
||||||
|
"dev.reset_delivery.done"
|
||||||
|
);
|
||||||
|
Ok(Json(DevResetDeliveryResponse {
|
||||||
|
belegnummer: query.belegnummer,
|
||||||
|
reset,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct DevReportQuery {
|
pub struct DevReportQuery {
|
||||||
/// UUID der Lieferung, für die der Report erzeugt werden soll.
|
/// UUID der Lieferung, für die der Report erzeugt werden soll.
|
||||||
|
|||||||
@ -6,7 +6,8 @@ use holzleitner_application::usecases::{
|
|||||||
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
|
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
|
||||||
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
|
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
|
||||||
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
||||||
DeleteServiceUseCase, DevResyncToursUseCase, GenerateDeliveryReportUseCase, GetAccountUseCase,
|
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
|
||||||
|
GenerateDeliveryReportUseCase, GetAccountUseCase,
|
||||||
GetAttachmentPreviewUseCase, GetTourUseCase,
|
GetAttachmentPreviewUseCase, GetTourUseCase,
|
||||||
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
|
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
|
||||||
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
||||||
@ -33,6 +34,8 @@ pub struct AppState {
|
|||||||
pub import_erp_tours: Arc<ImportErpToursUseCase>,
|
pub import_erp_tours: Arc<ImportErpToursUseCase>,
|
||||||
/// DEV-ONLY: überschreibender Resync (löscht Postgres + importiert neu).
|
/// DEV-ONLY: überschreibender Resync (löscht Postgres + importiert neu).
|
||||||
pub dev_resync_tours: Arc<DevResyncToursUseCase>,
|
pub dev_resync_tours: Arc<DevResyncToursUseCase>,
|
||||||
|
/// DEV-ONLY: setzt eine einzelne Lieferung (per Belegnummer) zurück.
|
||||||
|
pub dev_reset_delivery: Arc<DevResetDeliveryUseCase>,
|
||||||
/// Erzeugt den PDF-Lieferreport (lokal — Dev-Endpoint + Fallback ohne Upload).
|
/// Erzeugt den PDF-Lieferreport (lokal — Dev-Endpoint + Fallback ohne Upload).
|
||||||
pub generate_delivery_report: Arc<GenerateDeliveryReportUseCase>,
|
pub generate_delivery_report: Arc<GenerateDeliveryReportUseCase>,
|
||||||
/// Überträgt den Report an DOCUframe (Upload → Makro → Cleanup) — beim
|
/// Überträgt den Report an DOCUframe (Upload → Makro → Cleanup) — beim
|
||||||
|
|||||||
@ -55,4 +55,19 @@ pub trait TourRepository: Send + Sync {
|
|||||||
/// Dev-Resync, der die Postgres-Daten vor einem frischen Import platt
|
/// Dev-Resync, der die Postgres-Daten vor einem frischen Import platt
|
||||||
/// macht. Gibt die Anzahl gelöschter Touren zurück.
|
/// macht. Gibt die Anzahl gelöschter Touren zurück.
|
||||||
async fn delete_all_tours(&self) -> Result<u64, ApplicationError>;
|
async fn delete_all_tours(&self) -> Result<u64, ApplicationError>;
|
||||||
|
|
||||||
|
/// **DEV-ONLY**: Setzt eine **einzelne** Lieferung (über ihre ERP-
|
||||||
|
/// Belegnummer) zurück, ohne die Tour anzufassen — damit man den
|
||||||
|
/// Auslieferungs-Flow neu durchspielen kann:
|
||||||
|
/// * Abschluss (`delivery_completions`) gelöscht,
|
||||||
|
/// * Scan-/Gutschrift-Audit (`scan_audit`, `delivery_credit_audit`) gelöscht,
|
||||||
|
/// * Positionen zurück (scanned/credited = 0, Status `in_progress`),
|
||||||
|
/// * Lieferung wieder `active` (state_reason/assigned_car/review_* geleert).
|
||||||
|
///
|
||||||
|
/// Notizen, Dienstleistungen und Anhänge bleiben unangetastet. Gibt die
|
||||||
|
/// Anzahl zurückgesetzter Lieferungen zurück (0 = Belegnummer unbekannt).
|
||||||
|
async fn reset_delivery_by_belegnummer(
|
||||||
|
&self,
|
||||||
|
belegnummer: &str,
|
||||||
|
) -> Result<u64, ApplicationError>;
|
||||||
}
|
}
|
||||||
|
|||||||
32
crates/application/src/usecases/dev_reset_delivery.rs
Normal file
32
crates/application/src/usecases/dev_reset_delivery.rs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
//! DEV-ONLY: Einzelne Lieferung zurücksetzen (per Belegnummer).
|
||||||
|
//!
|
||||||
|
//! Erlaubt, den Auslieferungs-Flow einer Lieferung erneut durchzuspielen,
|
||||||
|
//! ohne die ganze Tour (`/dev/resync`) zu löschen: Abschluss, Scan-/
|
||||||
|
//! Gutschrift-Audit und Report-Job weg, Positionen + Lieferung zurück auf
|
||||||
|
//! `active`. Notizen/Dienstleistungen/Anhänge bleiben unangetastet.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::error::ApplicationError;
|
||||||
|
use crate::ports::TourRepository;
|
||||||
|
|
||||||
|
pub struct DevResetDeliveryUseCase {
|
||||||
|
tours: Arc<dyn TourRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DevResetDeliveryUseCase {
|
||||||
|
pub fn new(tours: Arc<dyn TourRepository>) -> Self {
|
||||||
|
Self { tours }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liefert die Anzahl zurückgesetzter Lieferungen (0 = Belegnummer unbekannt).
|
||||||
|
pub async fn execute(&self, belegnummer: &str) -> Result<u64, ApplicationError> {
|
||||||
|
let bn = belegnummer.trim();
|
||||||
|
if bn.is_empty() {
|
||||||
|
return Err(ApplicationError::Validation(
|
||||||
|
"belegnummer darf nicht leer sein".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.tours.reset_delivery_by_belegnummer(bn).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ pub mod cars;
|
|||||||
pub mod complete_delivery;
|
pub mod complete_delivery;
|
||||||
pub mod create_delivery_note;
|
pub mod create_delivery_note;
|
||||||
pub mod delete_delivery_note;
|
pub mod delete_delivery_note;
|
||||||
|
pub mod dev_reset_delivery;
|
||||||
pub mod dev_resync_tours;
|
pub mod dev_resync_tours;
|
||||||
pub mod generate_delivery_report;
|
pub mod generate_delivery_report;
|
||||||
pub mod get_account;
|
pub mod get_account;
|
||||||
@ -39,6 +40,7 @@ pub use cars::{
|
|||||||
};
|
};
|
||||||
pub use complete_delivery::CompleteDeliveryUseCase;
|
pub use complete_delivery::CompleteDeliveryUseCase;
|
||||||
pub use create_delivery_note::CreateDeliveryNoteUseCase;
|
pub use create_delivery_note::CreateDeliveryNoteUseCase;
|
||||||
|
pub use dev_reset_delivery::DevResetDeliveryUseCase;
|
||||||
pub use dev_resync_tours::DevResyncToursUseCase;
|
pub use dev_resync_tours::DevResyncToursUseCase;
|
||||||
pub use generate_delivery_report::GenerateDeliveryReportUseCase;
|
pub use generate_delivery_report::GenerateDeliveryReportUseCase;
|
||||||
pub use delete_delivery_note::DeleteDeliveryNoteUseCase;
|
pub use delete_delivery_note::DeleteDeliveryNoteUseCase;
|
||||||
|
|||||||
@ -912,6 +912,82 @@ impl TourRepository for PgTourRepository {
|
|||||||
.map_err(db)?;
|
.map_err(db)?;
|
||||||
Ok(res.rows_affected())
|
Ok(res.rows_affected())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn reset_delivery_by_belegnummer(
|
||||||
|
&self,
|
||||||
|
belegnummer: &str,
|
||||||
|
) -> Result<u64, ApplicationError> {
|
||||||
|
let mut tx = self.pool.begin().await.map_err(db)?;
|
||||||
|
|
||||||
|
// Subquery-Filter überall identisch: Lieferung(en) mit dieser Belegnr.
|
||||||
|
const BY_BELEG: &str =
|
||||||
|
"SELECT id FROM deliveries WHERE erp_belegnummer = $1";
|
||||||
|
|
||||||
|
// Abschluss + (evtl.) offener Report-Job + Scan-/Gutschrift-Audit löschen.
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"DELETE FROM delivery_report_jobs WHERE delivery_id IN ({BY_BELEG})"
|
||||||
|
))
|
||||||
|
.bind(belegnummer)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"DELETE FROM delivery_completions WHERE delivery_id IN ({BY_BELEG})"
|
||||||
|
))
|
||||||
|
.bind(belegnummer)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"DELETE FROM scan_audit WHERE delivery_item_id IN (\
|
||||||
|
SELECT di.id FROM delivery_items di \
|
||||||
|
JOIN deliveries d ON d.id = di.delivery_id \
|
||||||
|
WHERE d.erp_belegnummer = $1)",
|
||||||
|
)
|
||||||
|
.bind(belegnummer)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"DELETE FROM delivery_credit_audit WHERE delivery_id IN ({BY_BELEG})"
|
||||||
|
))
|
||||||
|
.bind(belegnummer)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
// Positionen zurück (verladen + gutgeschrieben = 0, Status offen).
|
||||||
|
sqlx::query(&format!(
|
||||||
|
"UPDATE delivery_items \
|
||||||
|
SET scanned_quantity = 0, scan_status = 'in_progress', \
|
||||||
|
held_reason = NULL, credited_quantity = 0, \
|
||||||
|
scan_last_updated_at = now() \
|
||||||
|
WHERE delivery_id IN ({BY_BELEG})"
|
||||||
|
))
|
||||||
|
.bind(belegnummer)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
// Lieferung wieder aktiv; Status-/Auto-/Review-Felder leeren.
|
||||||
|
let res = sqlx::query(
|
||||||
|
"UPDATE deliveries \
|
||||||
|
SET state = 'active', state_reason = NULL, assigned_car_id = NULL, \
|
||||||
|
review_resolved_at = NULL, review_resolved_by = NULL, \
|
||||||
|
review_note = NULL \
|
||||||
|
WHERE erp_belegnummer = $1",
|
||||||
|
)
|
||||||
|
.bind(belegnummer)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
tx.commit().await.map_err(db)?;
|
||||||
|
Ok(res.rows_affected())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Upsert-Helfer =====================================================
|
// ===== Upsert-Helfer =====================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user