feat(admin): GET /admin/completed-deliveries — abgeschlossene Lieferungen eines Tages

Neuer Admin-Endpunkt, der ALLE an einem Tag (Berliner Kalendertag von
completed_at) abgeschlossenen Lieferungen liefert — unabhängig vom
Mail-Versand-Status. Pro Lieferung: Belegnummer und ein Flag
`positions_modified`, das true ist, wenn eine Belegzeile in der Menge
reduziert/entfernt wurde (delivery_items.credited_quantity > 0) oder eine
aktive Geld-Gutschrift vorliegt (jüngstes delivery_credit_audit-Event 'set').

Pflichtparameter `day` (DD-MM-YYYY). Geschützt per admin_api_key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-07-09 16:06:01 +02:00
parent 819005eaa5
commit 748df577ef
9 changed files with 206 additions and 4 deletions

View File

@ -39,7 +39,8 @@ use holzleitner_application::usecases::{
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
GenerateDeliveryReportUseCase, GetAccountUseCase,
GetAttachmentPreviewUseCase, GetTourUseCase,
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
ImportErpToursUseCase, ListCompletedDeliveriesUseCase, ListDeliveredBelegnummernUseCase,
ListMyCarsUseCase,
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
ListPendingReviewsUseCase, ListServicesUseCase, MarkMailSentUseCase,
PushCompletionToErpUseCase, ResolveReviewUseCase,
@ -365,6 +366,11 @@ pub(crate) async fn run_app(
let list_delivered_belegnummern = Arc::new(ListDeliveredBelegnummernUseCase::new(
delivery_completion_repository.clone(),
));
// Admin-Lese-Use-Case: alle an einem Tag abgeschlossenen Lieferungen samt
// Positions-Änderungs-Flag. `.clone()` VOR dem Move in `complete_delivery`.
let list_completed_deliveries = Arc::new(ListCompletedDeliveriesUseCase::new(
delivery_completion_repository.clone(),
));
let mark_mail_sent = Arc::new(MarkMailSentUseCase::new(
delivery_completion_repository.clone(),
));
@ -460,6 +466,7 @@ pub(crate) async fn run_app(
complete_delivery,
push_completion_to_erp,
list_delivered_belegnummern,
list_completed_deliveries,
mark_mail_sent,
list_pending_reviews,
resolve_review,

View File

@ -54,6 +54,7 @@ use utoipa::openapi::security::{
crate::routes::admin::import_erp,
crate::routes::admin::push_completion,
crate::routes::admin::delivered_belegnummern,
crate::routes::admin::completed_deliveries,
crate::routes::admin::mark_mail_sent,
crate::routes::admin::list_reviews,
crate::routes::admin::resolve_review,
@ -127,6 +128,8 @@ use utoipa::openapi::security::{
holzleitner_application::dto::DeliveryServiceResponse,
holzleitner_application::usecases::ImportSummary,
crate::routes::admin::DeliveredBelegnummernResponse,
crate::routes::admin::CompletedDeliveriesResponse,
crate::routes::admin::CompletedDeliveryItem,
crate::routes::admin::PendingReviewResponse,
crate::routes::admin::ReviewedItemResponse,
crate::routes::admin::ResolveReviewRequest,

View File

@ -29,6 +29,10 @@ pub fn router() -> Router<AppState> {
"/admin/delivered-belegnummern",
get(delivered_belegnummern),
)
.route(
"/admin/completed-deliveries",
get(completed_deliveries),
)
.route("/admin/mark-mail-sent", post(mark_mail_sent))
.route("/admin/reviews", get(list_reviews))
.route("/admin/reviews/{delivery_id}/resolve", post(resolve_review))
@ -186,6 +190,80 @@ pub async fn delivered_belegnummern(
}))
}
#[derive(Debug, Deserialize)]
pub struct CompletedDeliveriesQuery {
/// Ziel-Tag im Format `DD-MM-YYYY`. **Pflicht** — der Endpunkt liefert nur
/// Abschlüsse genau dieses (Berliner) Kalendertages.
pub day: String,
}
/// Eine abgeschlossene Lieferung im Tagesabruf.
#[derive(Debug, Serialize, ToSchema)]
pub struct CompletedDeliveryItem {
/// ERP-Belegnummer der Lieferung.
pub belegnummer: String,
/// `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile
/// wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es
/// liegt eine aktive Geld-Gutschrift vor.
pub positions_modified: bool,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct CompletedDeliveriesResponse {
/// Tag, nach dem gefiltert wurde (ISO `YYYY-MM-DD`).
pub day: String,
/// Anzahl der abgeschlossenen Lieferungen an diesem Tag.
pub count: usize,
/// Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt.
pub deliveries: Vec<CompletedDeliveryItem>,
}
/// Liefert **alle** an einem Tag abgeschlossenen (ausgelieferten) Lieferungen —
/// unabhängig vom Mail-Versand-Status. Der Tag (`day`, `DD-MM-YYYY`) ist Pflicht
/// und wird als Berliner Kalendertag auf den Abschluss-Zeitpunkt
/// (`completed_at`) angewandt. Pro Lieferung: Belegnummer und ein Flag, ob
/// Positionen verändert wurden (Menge reduziert/Zeile entfernt oder
/// Geld-Gutschrift).
#[utoipa::path(
get,
path = "/admin/completed-deliveries",
tag = "admin",
params(
("day" = String, Query, description = "Tag DD-MM-YYYY (Pflicht)")
),
responses(
(status = 200, description = "Abgeschlossene Lieferungen des Tages", body = CompletedDeliveriesResponse),
(status = 400, description = "Ungültiger/fehlender Tag"),
(status = 401, description = "Admin-API-Key fehlt/ungültig")
),
security(("admin_api_key" = []))
)]
pub async fn completed_deliveries(
State(state): State<AppState>,
Query(query): Query<CompletedDeliveriesQuery>,
) -> Result<Json<CompletedDeliveriesResponse>, ApiError> {
let day = NaiveDate::parse_from_str(query.day.trim(), "%d-%m-%Y").map_err(|e| {
ApiError(ApplicationError::Validation(format!(
"ungültiger Tag '{}' (erwartet DD-MM-YYYY): {e}",
query.day
)))
})?;
tracing::info!(?day, "admin.completed_deliveries");
let summaries = state.list_completed_deliveries.execute(day).await?;
tracing::info!(?day, count = summaries.len(), "admin.completed_deliveries.done");
Ok(Json(CompletedDeliveriesResponse {
day: day.format("%Y-%m-%d").to_string(),
count: summaries.len(),
deliveries: summaries
.into_iter()
.map(|s| CompletedDeliveryItem {
belegnummer: s.belegnummer,
positions_modified: s.positions_modified,
})
.collect(),
}))
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct MarkMailSentRequest {
/// Belegnummern, deren Liefermail erfolgreich versendet wurde und die als

View File

@ -9,7 +9,8 @@ use holzleitner_application::usecases::{
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
GenerateDeliveryReportUseCase, GetAccountUseCase,
GetAttachmentPreviewUseCase, GetTourUseCase,
ImportErpToursUseCase, ListDeliveredBelegnummernUseCase, ListMyCarsUseCase,
ImportErpToursUseCase, ListCompletedDeliveriesUseCase, ListDeliveredBelegnummernUseCase,
ListMyCarsUseCase,
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
ListPendingReviewsUseCase, ListServicesUseCase, MarkMailSentUseCase,
ProcessDeliveryReportUseCase, PushCompletionToErpUseCase, ResolveReviewUseCase,
@ -51,6 +52,8 @@ pub struct AppState {
pub push_completion_to_erp: Arc<PushCompletionToErpUseCase>,
/// Admin: Belegnummern offener (noch nicht versendeter) Lieferungen.
pub list_delivered_belegnummern: Arc<ListDeliveredBelegnummernUseCase>,
/// Admin: alle an einem Tag abgeschlossenen Lieferungen (mit Änderungs-Flag).
pub list_completed_deliveries: Arc<ListCompletedDeliveriesUseCase>,
/// Admin: Liefermails von Belegnummern als versendet markieren (Dedup).
pub mark_mail_sent: Arc<MarkMailSentUseCase>,
pub list_pending_reviews: Arc<ListPendingReviewsUseCase>,