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:
Dennis Nemec
2026-06-24 13:35:45 +02:00
parent fb5f43ed7a
commit 819005eaa5
7 changed files with 173 additions and 2 deletions

View File

@ -36,7 +36,8 @@ use holzleitner_application::usecases::{
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
DeleteServiceUseCase, DevResyncToursUseCase, GenerateDeliveryReportUseCase, GetAccountUseCase,
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
GenerateDeliveryReportUseCase, GetAccountUseCase,
GetAttachmentPreviewUseCase, GetTourUseCase,
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
@ -345,6 +346,9 @@ pub(crate) async fn run_app(
tour_repository.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
// gebaut (Admin-Retry-Endpunkt nutzt ihn manuell). Ob der normale
// Abschluss-Pfad automatisch pusht, steuert `ERP_WRITEBACK_ENABLED`.
@ -447,6 +451,7 @@ pub(crate) async fn run_app(
set_delivery_order,
import_erp_tours: import_erp_tours.clone(),
dev_resync_tours,
dev_reset_delivery,
generate_delivery_report,
process_delivery_report: process_delivery_report.clone(),
report_upload_enabled: cfg.report.upload_enabled,

View File

@ -24,6 +24,7 @@ use crate::state::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/dev/resync", post(dev_resync))
.route("/dev/reset-delivery", post(dev_reset_delivery))
.route("/dev/generate-report", post(dev_generate_report))
.route("/dev/process-report", post(dev_process_report))
.route("/dev/unmark-mail-sent", post(dev_unmark_mail_sent))
@ -64,6 +65,43 @@ pub async fn dev_resync(
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)]
pub struct DevReportQuery {
/// UUID der Lieferung, für die der Report erzeugt werden soll.

View File

@ -6,7 +6,8 @@ use holzleitner_application::usecases::{
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
DeleteServiceUseCase, DevResyncToursUseCase, GenerateDeliveryReportUseCase, GetAccountUseCase,
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
GenerateDeliveryReportUseCase, GetAccountUseCase,
GetAttachmentPreviewUseCase, GetTourUseCase,
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
@ -33,6 +34,8 @@ pub struct AppState {
pub import_erp_tours: Arc<ImportErpToursUseCase>,
/// DEV-ONLY: überschreibender Resync (löscht Postgres + importiert neu).
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).
pub generate_delivery_report: Arc<GenerateDeliveryReportUseCase>,
/// Überträgt den Report an DOCUframe (Upload → Makro → Cleanup) — beim