Neuer Admin-Endpunkt, der zu EINER ERP-Belegnummer das positions_modified-Flag liefert (Stück-Gutschrift auf einer Belegzeile ODER aktive Geld-Gutschrift) — gleiche Definition wie in completed-deliveries, aber pro Beleg und ohne Abschluss-Voraussetzung. 404 bei unbekannter Belegnummer; bei mehreren Lieferungen gleicher Belegnummer true, sobald eine verändert ist (bool_or). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
512 lines
19 KiB
Rust
512 lines
19 KiB
Rust
//! Admin-/Betriebs-Endpunkte.
|
|
//!
|
|
//! Aktuell: manueller ERP-Import-Trigger. Derselbe Use Case, den auch der
|
|
//! tägliche Scheduler ruft — hier on-demand für ein konkretes Datum
|
|
//! (Testen + manuelle Nachläufe im Betrieb). JWT-geschützt wie alle
|
|
//! protected Routen.
|
|
|
|
use axum::Json;
|
|
use axum::Router;
|
|
use axum::extract::{Path, Query, State};
|
|
use axum::http::StatusCode;
|
|
use axum::routing::{get, post};
|
|
use chrono::NaiveDate;
|
|
use serde::{Deserialize, Serialize};
|
|
use utoipa::ToSchema;
|
|
use uuid::Uuid;
|
|
|
|
use holzleitner_application::error::ApplicationError;
|
|
use holzleitner_application::usecases::ImportSummary;
|
|
|
|
use crate::error::ApiError;
|
|
use crate::state::AppState;
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
Router::new()
|
|
.route("/admin/import-erp", post(import_erp))
|
|
.route("/admin/push-completion", post(push_completion))
|
|
.route(
|
|
"/admin/delivered-belegnummern",
|
|
get(delivered_belegnummern),
|
|
)
|
|
.route(
|
|
"/admin/completed-deliveries",
|
|
get(completed_deliveries),
|
|
)
|
|
.route(
|
|
"/admin/belege/{belegnummer}/positions-modified",
|
|
get(positions_modified),
|
|
)
|
|
.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)]
|
|
pub struct ImportErpQuery {
|
|
/// Ziel-Tourdatum `YYYY-MM-DD`. Fehlt der Parameter, wird **heute**
|
|
/// verwendet.
|
|
#[serde(default)]
|
|
pub date: Option<String>,
|
|
}
|
|
|
|
/// Stößt den ERP-Import für ein Datum an und liefert die Zusammenfassung.
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/admin/import-erp",
|
|
tag = "admin",
|
|
params(
|
|
("date" = Option<String>, Query, description = "Ziel-Tourdatum YYYY-MM-DD (Default: heute)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Import durchgeführt", body = ImportSummary),
|
|
(status = 400, description = "Ungültiges Datum"),
|
|
(status = 401, description = "Admin-API-Key fehlt/ungültig"),
|
|
(status = 502, description = "ERP nicht erreichbar / Lesefehler")
|
|
),
|
|
security(("admin_api_key" = []))
|
|
)]
|
|
pub async fn import_erp(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<ImportErpQuery>,
|
|
) -> Result<Json<ImportSummary>, ApiError> {
|
|
let date = match query.date {
|
|
Some(s) => NaiveDate::parse_from_str(s.trim(), "%Y-%m-%d").map_err(|e| {
|
|
ApiError(ApplicationError::Validation(format!(
|
|
"ungültiges Datum '{s}' (erwartet YYYY-MM-DD): {e}"
|
|
)))
|
|
})?,
|
|
None => chrono::Utc::now().date_naive(),
|
|
};
|
|
tracing::info!(%date, "admin.import_erp");
|
|
let summary = state.import_erp_tours.execute(date).await?;
|
|
tracing::info!(
|
|
%date,
|
|
total = summary.tours_total,
|
|
ok = summary.tours_ok,
|
|
failed = summary.tours_failed,
|
|
"admin.import_erp.done"
|
|
);
|
|
Ok(Json(summary))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PushCompletionQuery {
|
|
/// UUID der bereits abgeschlossenen Lieferung.
|
|
pub delivery_id: String,
|
|
}
|
|
|
|
/// Stößt das ERP-Rückschreiben eines bereits lokal abgeschlossenen
|
|
/// Lieferabschlusses erneut an (idempotenter Retry, falls der automatische
|
|
/// Push beim Abschluss fehlschlug).
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/admin/push-completion",
|
|
tag = "admin",
|
|
params(
|
|
("delivery_id" = String, Query, description = "UUID der abgeschlossenen Lieferung")
|
|
),
|
|
responses(
|
|
(status = 204, description = "Rückschreiben erfolgreich"),
|
|
(status = 400, description = "Ungültige delivery_id"),
|
|
(status = 401, description = "Admin-API-Key fehlt/ungültig"),
|
|
(status = 404, description = "Lieferung nicht gefunden / nicht abgeschlossen"),
|
|
(status = 502, description = "ERP nicht erreichbar / Schreibfehler")
|
|
),
|
|
security(("admin_api_key" = []))
|
|
)]
|
|
pub async fn push_completion(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<PushCompletionQuery>,
|
|
) -> Result<StatusCode, ApiError> {
|
|
let delivery_id = Uuid::parse_str(query.delivery_id.trim()).map_err(|e| {
|
|
ApiError(ApplicationError::Validation(format!(
|
|
"ungültige delivery_id '{}': {e}",
|
|
query.delivery_id
|
|
)))
|
|
})?;
|
|
tracing::info!(%delivery_id, "admin.push_completion");
|
|
state.push_completion_to_erp.execute(delivery_id).await?;
|
|
tracing::info!(%delivery_id, "admin.push_completion.done");
|
|
Ok(StatusCode::NO_CONTENT)
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeliveredBelegnummernQuery {
|
|
/// Ziel-Tag im Format `DD-MM-YYYY`. **Fehlt der Parameter, werden ALLE**
|
|
/// offenen (noch nicht versendeten) Belege über alle Tage geliefert — das
|
|
/// ist der Modus des Mailclients.
|
|
#[serde(default)]
|
|
pub day: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, ToSchema)]
|
|
pub struct DeliveredBelegnummernResponse {
|
|
/// Tag, nach dem gefiltert wurde (ISO `YYYY-MM-DD`), oder `"all"` wenn kein
|
|
/// `day` angegeben war.
|
|
pub day: String,
|
|
/// Anzahl der offenen (noch nicht versendeten) Belege.
|
|
pub count: usize,
|
|
/// Belegnummern aller **ausgelieferten** (abgeschlossenen) Lieferungen,
|
|
/// deren Liefermail noch **nicht versendet** wurde, aufsteigend nach
|
|
/// Abschluss-Zeitpunkt.
|
|
pub belegnummern: Vec<String>,
|
|
}
|
|
|
|
/// Liefert die Belegnummern ausgelieferter (abgeschlossener) Lieferungen,
|
|
/// **deren Liefermail noch nicht versendet wurde** (`mail_sent_at IS NULL`).
|
|
/// „Ausgeliefert" = es existiert ein Abschluss. Mit `day` (DD-MM-YYYY) nur
|
|
/// Abschlüsse dieses Berliner Kalendertages; **ohne `day` alle offenen** (über
|
|
/// alle Tage) — so bleiben Belege über Mitternacht nicht hängen.
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/admin/delivered-belegnummern",
|
|
tag = "admin",
|
|
params(
|
|
("day" = Option<String>, Query, description = "Tag DD-MM-YYYY; ohne Angabe ALLE offenen Belege")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Offene (nicht versendete) Belegnummern", body = DeliveredBelegnummernResponse),
|
|
(status = 400, description = "Ungültiger Tag"),
|
|
(status = 401, description = "Admin-API-Key fehlt/ungültig")
|
|
),
|
|
security(("admin_api_key" = []))
|
|
)]
|
|
pub async fn delivered_belegnummern(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<DeliveredBelegnummernQuery>,
|
|
) -> Result<Json<DeliveredBelegnummernResponse>, ApiError> {
|
|
let day: Option<NaiveDate> = match query.day {
|
|
Some(s) => Some(NaiveDate::parse_from_str(s.trim(), "%d-%m-%Y").map_err(|e| {
|
|
ApiError(ApplicationError::Validation(format!(
|
|
"ungültiger Tag '{s}' (erwartet DD-MM-YYYY): {e}"
|
|
)))
|
|
})?),
|
|
None => None,
|
|
};
|
|
tracing::info!(?day, "admin.delivered_belegnummern");
|
|
let belegnummern = state.list_delivered_belegnummern.execute(day).await?;
|
|
tracing::info!(?day, count = belegnummern.len(), "admin.delivered_belegnummern.done");
|
|
Ok(Json(DeliveredBelegnummernResponse {
|
|
day: day.map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or_else(|| "all".into()),
|
|
count: belegnummern.len(),
|
|
belegnummern,
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CompletedDeliveriesQuery {
|
|
/// Einzeltag `DD-MM-YYYY` — Kurzform für `from == to == day`. Hat Vorrang
|
|
/// vor `from`/`to`, wenn gesetzt.
|
|
#[serde(default)]
|
|
pub day: Option<String>,
|
|
/// Untere Bereichsgrenze `DD-MM-YYYY` (inklusive). Ohne Angabe offen.
|
|
#[serde(default)]
|
|
pub from: Option<String>,
|
|
/// Obere Bereichsgrenze `DD-MM-YYYY` (inklusive). Ohne Angabe offen.
|
|
#[serde(default)]
|
|
pub to: Option<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 {
|
|
/// Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen).
|
|
pub from: Option<String>,
|
|
/// Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen).
|
|
pub to: Option<String>,
|
|
/// Anzahl der abgeschlossenen Lieferungen im Bereich.
|
|
pub count: usize,
|
|
/// Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt.
|
|
pub deliveries: Vec<CompletedDeliveryItem>,
|
|
}
|
|
|
|
/// Parst einen `DD-MM-YYYY`-Tag in ein `NaiveDate` oder liefert `400`.
|
|
fn parse_ddmmyyyy(s: &str) -> Result<NaiveDate, ApiError> {
|
|
NaiveDate::parse_from_str(s.trim(), "%d-%m-%Y").map_err(|e| {
|
|
ApiError(ApplicationError::Validation(format!(
|
|
"ungültiges Datum '{s}' (erwartet DD-MM-YYYY): {e}"
|
|
)))
|
|
})
|
|
}
|
|
|
|
/// Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen in einem
|
|
/// Datumsbereich — unabhängig vom Mail-Versand-Status. Gefiltert wird über den
|
|
/// **Berliner** Kalendertag des Abschluss-Zeitpunkts (`completed_at`).
|
|
///
|
|
/// Parameter (alle `DD-MM-YYYY`): `day` = Einzeltag (Kurzform `from=to=day`),
|
|
/// sonst `from`/`to` als **inklusive** Bereichsgrenzen (je optional/offen).
|
|
/// Mindestens einer von `day`/`from`/`to` ist erforderlich. Pro Lieferung:
|
|
/// Belegnummer + `positions_modified` (Menge reduziert/Zeile entfernt oder
|
|
/// Geld-Gutschrift). Die Halb-Grenzen-Variante ist für Range-Filter gedacht:
|
|
/// `?from=…` und `?to=…` liefern je eine Menge, deren SQL-`AND`-Schnitt den
|
|
/// Zeitraum ergibt.
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/admin/completed-deliveries",
|
|
tag = "admin",
|
|
params(
|
|
("day" = Option<String>, Query, description = "Einzeltag DD-MM-YYYY (Kurzform from=to)"),
|
|
("from" = Option<String>, Query, description = "Untere Grenze DD-MM-YYYY (inklusive)"),
|
|
("to" = Option<String>, Query, description = "Obere Grenze DD-MM-YYYY (inklusive)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Abgeschlossene Lieferungen im Bereich", body = CompletedDeliveriesResponse),
|
|
(status = 400, description = "Ungültiges Datum oder keine Grenze angegeben"),
|
|
(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> {
|
|
// `day` ist die Kurzform und hat Vorrang; sonst freie Halb-/Vollgrenzen.
|
|
let (from, to) = match query.day.as_deref() {
|
|
Some(day) => {
|
|
let d = parse_ddmmyyyy(day)?;
|
|
(Some(d), Some(d))
|
|
}
|
|
None => (
|
|
query.from.as_deref().map(parse_ddmmyyyy).transpose()?,
|
|
query.to.as_deref().map(parse_ddmmyyyy).transpose()?,
|
|
),
|
|
};
|
|
|
|
// Unbegrenzt (alles) wäre ein versehentlicher Full-Table-Dump → ablehnen.
|
|
if from.is_none() && to.is_none() {
|
|
return Err(ApiError(ApplicationError::Validation(
|
|
"mindestens einer von `day`, `from`, `to` (DD-MM-YYYY) ist erforderlich".into(),
|
|
)));
|
|
}
|
|
|
|
tracing::info!(?from, ?to, "admin.completed_deliveries");
|
|
let summaries = state.list_completed_deliveries.execute(from, to).await?;
|
|
tracing::info!(?from, ?to, count = summaries.len(), "admin.completed_deliveries.done");
|
|
Ok(Json(CompletedDeliveriesResponse {
|
|
from: from.map(|d| d.format("%Y-%m-%d").to_string()),
|
|
to: to.map(|d| d.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, Serialize, ToSchema)]
|
|
pub struct PositionsModifiedResponse {
|
|
/// ERP-Belegnummer, nach der gefragt wurde.
|
|
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,
|
|
}
|
|
|
|
/// Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen
|
|
/// verändert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) —
|
|
/// unabhängig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt
|
|
/// ist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag
|
|
/// `true`, sobald **eine** davon verändert ist.
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/admin/belege/{belegnummer}/positions-modified",
|
|
tag = "admin",
|
|
params(
|
|
("belegnummer" = String, Path, description = "ERP-Belegnummer, z. B. V-30690291")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Änderungs-Flag der Belegpositionen", body = PositionsModifiedResponse),
|
|
(status = 401, description = "Admin-API-Key fehlt/ungültig"),
|
|
(status = 404, description = "Belegnummer unbekannt")
|
|
),
|
|
security(("admin_api_key" = []))
|
|
)]
|
|
pub async fn positions_modified(
|
|
State(state): State<AppState>,
|
|
Path(belegnummer): Path<String>,
|
|
) -> Result<Json<PositionsModifiedResponse>, ApiError> {
|
|
tracing::info!(%belegnummer, "admin.positions_modified");
|
|
let flag = state
|
|
.get_positions_modified
|
|
.execute(&belegnummer)
|
|
.await?
|
|
.ok_or(ApiError(ApplicationError::NotFound))?;
|
|
tracing::info!(%belegnummer, positions_modified = flag, "admin.positions_modified.done");
|
|
Ok(Json(PositionsModifiedResponse {
|
|
belegnummer,
|
|
positions_modified: flag,
|
|
}))
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, ToSchema)]
|
|
pub struct MarkMailSentRequest {
|
|
/// Belegnummern, deren Liefermail erfolgreich versendet wurde und die als
|
|
/// versendet markiert werden sollen.
|
|
pub belegnummern: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, ToSchema)]
|
|
pub struct MarkMailSentResponse {
|
|
/// Anzahl frisch markierter (vorher offener) Belege. Bereits markierte
|
|
/// zählen nicht mit (idempotent).
|
|
pub marked: u64,
|
|
}
|
|
|
|
/// Markiert die Liefermails der angegebenen Belegnummern als **versendet**
|
|
/// (`mail_sent_at = now()`, nur wo noch offen). Vom Mailclient aufzurufen,
|
|
/// NACHDEM ERPframe die Mails erfolgreich verschickt hat — danach erscheinen
|
|
/// die Belege nicht mehr in `GET /admin/delivered-belegnummern`.
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/admin/mark-mail-sent",
|
|
tag = "admin",
|
|
request_body = MarkMailSentRequest,
|
|
responses(
|
|
(status = 200, description = "Markierung durchgeführt", body = MarkMailSentResponse),
|
|
(status = 401, description = "Admin-API-Key fehlt/ungültig")
|
|
),
|
|
security(("admin_api_key" = []))
|
|
)]
|
|
pub async fn mark_mail_sent(
|
|
State(state): State<AppState>,
|
|
Json(body): Json<MarkMailSentRequest>,
|
|
) -> Result<Json<MarkMailSentResponse>, ApiError> {
|
|
tracing::info!(count = body.belegnummern.len(), "admin.mark_mail_sent");
|
|
let marked = state.mark_mail_sent.execute(body.belegnummern).await?;
|
|
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)
|
|
}
|