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

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