feat(admin): GET /admin/belege/{belegnummer}/positions-modified
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>
This commit is contained in:
@ -38,7 +38,7 @@ use holzleitner_application::usecases::{
|
|||||||
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
||||||
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
|
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
|
||||||
GenerateDeliveryReportUseCase, GetAccountUseCase,
|
GenerateDeliveryReportUseCase, GetAccountUseCase,
|
||||||
GetAttachmentPreviewUseCase, GetTourUseCase,
|
GetAttachmentPreviewUseCase, GetPositionsModifiedUseCase, GetTourUseCase,
|
||||||
ImportErpToursUseCase, ListCompletedDeliveriesUseCase, ListDeliveredBelegnummernUseCase,
|
ImportErpToursUseCase, ListCompletedDeliveriesUseCase, ListDeliveredBelegnummernUseCase,
|
||||||
ListMyCarsUseCase,
|
ListMyCarsUseCase,
|
||||||
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
||||||
@ -371,6 +371,11 @@ pub(crate) async fn run_app(
|
|||||||
let list_completed_deliveries = Arc::new(ListCompletedDeliveriesUseCase::new(
|
let list_completed_deliveries = Arc::new(ListCompletedDeliveriesUseCase::new(
|
||||||
delivery_completion_repository.clone(),
|
delivery_completion_repository.clone(),
|
||||||
));
|
));
|
||||||
|
// Admin-Lese-Use-Case: Positions-Änderungs-Flag zu einer Belegnummer.
|
||||||
|
// `.clone()` VOR dem Move in `complete_delivery`.
|
||||||
|
let get_positions_modified = Arc::new(GetPositionsModifiedUseCase::new(
|
||||||
|
delivery_completion_repository.clone(),
|
||||||
|
));
|
||||||
let mark_mail_sent = Arc::new(MarkMailSentUseCase::new(
|
let mark_mail_sent = Arc::new(MarkMailSentUseCase::new(
|
||||||
delivery_completion_repository.clone(),
|
delivery_completion_repository.clone(),
|
||||||
));
|
));
|
||||||
@ -467,6 +472,7 @@ pub(crate) async fn run_app(
|
|||||||
push_completion_to_erp,
|
push_completion_to_erp,
|
||||||
list_delivered_belegnummern,
|
list_delivered_belegnummern,
|
||||||
list_completed_deliveries,
|
list_completed_deliveries,
|
||||||
|
get_positions_modified,
|
||||||
mark_mail_sent,
|
mark_mail_sent,
|
||||||
list_pending_reviews,
|
list_pending_reviews,
|
||||||
resolve_review,
|
resolve_review,
|
||||||
|
|||||||
@ -55,6 +55,7 @@ use utoipa::openapi::security::{
|
|||||||
crate::routes::admin::push_completion,
|
crate::routes::admin::push_completion,
|
||||||
crate::routes::admin::delivered_belegnummern,
|
crate::routes::admin::delivered_belegnummern,
|
||||||
crate::routes::admin::completed_deliveries,
|
crate::routes::admin::completed_deliveries,
|
||||||
|
crate::routes::admin::positions_modified,
|
||||||
crate::routes::admin::mark_mail_sent,
|
crate::routes::admin::mark_mail_sent,
|
||||||
crate::routes::admin::list_reviews,
|
crate::routes::admin::list_reviews,
|
||||||
crate::routes::admin::resolve_review,
|
crate::routes::admin::resolve_review,
|
||||||
@ -130,6 +131,7 @@ use utoipa::openapi::security::{
|
|||||||
crate::routes::admin::DeliveredBelegnummernResponse,
|
crate::routes::admin::DeliveredBelegnummernResponse,
|
||||||
crate::routes::admin::CompletedDeliveriesResponse,
|
crate::routes::admin::CompletedDeliveriesResponse,
|
||||||
crate::routes::admin::CompletedDeliveryItem,
|
crate::routes::admin::CompletedDeliveryItem,
|
||||||
|
crate::routes::admin::PositionsModifiedResponse,
|
||||||
crate::routes::admin::PendingReviewResponse,
|
crate::routes::admin::PendingReviewResponse,
|
||||||
crate::routes::admin::ReviewedItemResponse,
|
crate::routes::admin::ReviewedItemResponse,
|
||||||
crate::routes::admin::ResolveReviewRequest,
|
crate::routes::admin::ResolveReviewRequest,
|
||||||
|
|||||||
@ -33,6 +33,10 @@ pub fn router() -> Router<AppState> {
|
|||||||
"/admin/completed-deliveries",
|
"/admin/completed-deliveries",
|
||||||
get(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/mark-mail-sent", post(mark_mail_sent))
|
||||||
.route("/admin/reviews", get(list_reviews))
|
.route("/admin/reviews", get(list_reviews))
|
||||||
.route("/admin/reviews/{delivery_id}/resolve", post(resolve_review))
|
.route("/admin/reviews/{delivery_id}/resolve", post(resolve_review))
|
||||||
@ -303,6 +307,52 @@ pub async fn completed_deliveries(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
pub struct MarkMailSentRequest {
|
pub struct MarkMailSentRequest {
|
||||||
/// Belegnummern, deren Liefermail erfolgreich versendet wurde und die als
|
/// Belegnummern, deren Liefermail erfolgreich versendet wurde und die als
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use holzleitner_application::usecases::{
|
|||||||
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
||||||
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
|
DeleteServiceUseCase, DevResetDeliveryUseCase, DevResyncToursUseCase,
|
||||||
GenerateDeliveryReportUseCase, GetAccountUseCase,
|
GenerateDeliveryReportUseCase, GetAccountUseCase,
|
||||||
GetAttachmentPreviewUseCase, GetTourUseCase,
|
GetAttachmentPreviewUseCase, GetPositionsModifiedUseCase, GetTourUseCase,
|
||||||
ImportErpToursUseCase, ListCompletedDeliveriesUseCase, ListDeliveredBelegnummernUseCase,
|
ImportErpToursUseCase, ListCompletedDeliveriesUseCase, ListDeliveredBelegnummernUseCase,
|
||||||
ListMyCarsUseCase,
|
ListMyCarsUseCase,
|
||||||
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
ListMyToursTodayUseCase, ListPaymentMethodsUseCase,
|
||||||
@ -54,6 +54,8 @@ pub struct AppState {
|
|||||||
pub list_delivered_belegnummern: Arc<ListDeliveredBelegnummernUseCase>,
|
pub list_delivered_belegnummern: Arc<ListDeliveredBelegnummernUseCase>,
|
||||||
/// Admin: alle an einem Tag abgeschlossenen Lieferungen (mit Änderungs-Flag).
|
/// Admin: alle an einem Tag abgeschlossenen Lieferungen (mit Änderungs-Flag).
|
||||||
pub list_completed_deliveries: Arc<ListCompletedDeliveriesUseCase>,
|
pub list_completed_deliveries: Arc<ListCompletedDeliveriesUseCase>,
|
||||||
|
/// Admin: Positions-Änderungs-Flag zu einer einzelnen Belegnummer.
|
||||||
|
pub get_positions_modified: Arc<GetPositionsModifiedUseCase>,
|
||||||
/// Admin: Liefermails von Belegnummern als versendet markieren (Dedup).
|
/// Admin: Liefermails von Belegnummern als versendet markieren (Dedup).
|
||||||
pub mark_mail_sent: Arc<MarkMailSentUseCase>,
|
pub mark_mail_sent: Arc<MarkMailSentUseCase>,
|
||||||
pub list_pending_reviews: Arc<ListPendingReviewsUseCase>,
|
pub list_pending_reviews: Arc<ListPendingReviewsUseCase>,
|
||||||
|
|||||||
@ -148,6 +148,20 @@ pub trait DeliveryCompletionRepository: Send + Sync {
|
|||||||
to: Option<NaiveDate>,
|
to: Option<NaiveDate>,
|
||||||
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError>;
|
) -> Result<Vec<CompletedDeliverySummary>, ApplicationError>;
|
||||||
|
|
||||||
|
/// Liefert zu einer ERP-Belegnummer, ob an der Lieferung **Positionen
|
||||||
|
/// verändert** wurden — dieselbe Definition wie `positions_modified` in
|
||||||
|
/// [`CompletedDeliverySummary`] (Stück-Gutschrift auf einer Belegzeile
|
||||||
|
/// **oder** aktive Geld-Gutschrift). Zustand der Lieferung egal (kein
|
||||||
|
/// Abschluss vorausgesetzt).
|
||||||
|
///
|
||||||
|
/// `Ok(None)` ⇒ Belegnummer unbekannt (keine Lieferung). Bei mehreren
|
||||||
|
/// Lieferungen mit derselben Belegnummer gilt `true`, sobald **eine** davon
|
||||||
|
/// verändert ist.
|
||||||
|
async fn positions_modified_for_belegnummer(
|
||||||
|
&self,
|
||||||
|
belegnummer: &str,
|
||||||
|
) -> Result<Option<bool>, ApplicationError>;
|
||||||
|
|
||||||
/// Markiert die Liefermail der angegebenen Belegnummern als **versendet**
|
/// Markiert die Liefermail der angegebenen Belegnummern als **versendet**
|
||||||
/// (`mail_sent_at = now()`), aber nur dort, wo sie noch offen ist
|
/// (`mail_sent_at = now()`), aber nur dort, wo sie noch offen ist
|
||||||
/// (`mail_sent_at IS NULL`) — bereits markierte bleiben unverändert
|
/// (`mail_sent_at IS NULL`) — bereits markierte bleiben unverändert
|
||||||
|
|||||||
30
crates/application/src/usecases/get_positions_modified.rs
Normal file
30
crates/application/src/usecases/get_positions_modified.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//! Use Case: zu einer ERP-Belegnummer ermitteln, ob an der Lieferung
|
||||||
|
//! Positionen verändert wurden (Stück-Gutschrift oder aktive Geld-Gutschrift).
|
||||||
|
//!
|
||||||
|
//! Reine Lese-Operation für den Admin-Endpunkt. `Ok(None)` ⇒ Belegnummer
|
||||||
|
//! unbekannt (der Handler macht daraus ein `404`).
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::error::ApplicationError;
|
||||||
|
use crate::ports::DeliveryCompletionRepository;
|
||||||
|
|
||||||
|
pub struct GetPositionsModifiedUseCase {
|
||||||
|
completions: Arc<dyn DeliveryCompletionRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetPositionsModifiedUseCase {
|
||||||
|
pub fn new(completions: Arc<dyn DeliveryCompletionRepository>) -> Self {
|
||||||
|
Self { completions }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Some(true/false)` = Beleg gefunden, Flag; `None` = Belegnummer unbekannt.
|
||||||
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
belegnummer: &str,
|
||||||
|
) -> Result<Option<bool>, ApplicationError> {
|
||||||
|
self.completions
|
||||||
|
.positions_modified_for_belegnummer(belegnummer)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,7 @@ pub mod get_account;
|
|||||||
pub mod get_attachment_preview;
|
pub mod get_attachment_preview;
|
||||||
pub mod get_tour;
|
pub mod get_tour;
|
||||||
pub mod import_erp_tours;
|
pub mod import_erp_tours;
|
||||||
|
pub mod get_positions_modified;
|
||||||
pub mod list_completed_deliveries;
|
pub mod list_completed_deliveries;
|
||||||
pub mod list_delivered_belegnummern;
|
pub mod list_delivered_belegnummern;
|
||||||
pub mod list_my_tours_today;
|
pub mod list_my_tours_today;
|
||||||
@ -47,6 +48,7 @@ pub use generate_delivery_report::GenerateDeliveryReportUseCase;
|
|||||||
pub use delete_delivery_note::DeleteDeliveryNoteUseCase;
|
pub use delete_delivery_note::DeleteDeliveryNoteUseCase;
|
||||||
pub use get_account::GetAccountUseCase;
|
pub use get_account::GetAccountUseCase;
|
||||||
pub use get_attachment_preview::GetAttachmentPreviewUseCase;
|
pub use get_attachment_preview::GetAttachmentPreviewUseCase;
|
||||||
|
pub use get_positions_modified::GetPositionsModifiedUseCase;
|
||||||
pub use get_tour::GetTourUseCase;
|
pub use get_tour::GetTourUseCase;
|
||||||
pub use import_erp_tours::{ImportErpToursUseCase, ImportSummary};
|
pub use import_erp_tours::{ImportErpToursUseCase, ImportSummary};
|
||||||
pub use list_completed_deliveries::ListCompletedDeliveriesUseCase;
|
pub use list_completed_deliveries::ListCompletedDeliveriesUseCase;
|
||||||
|
|||||||
@ -530,6 +530,44 @@ impl DeliveryCompletionRepository for PgDeliveryCompletionRepository {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn positions_modified_for_belegnummer(
|
||||||
|
&self,
|
||||||
|
belegnummer: &str,
|
||||||
|
) -> Result<Option<bool>, ApplicationError> {
|
||||||
|
// `positions_modified`-Logik identisch zu list_completed_deliveries,
|
||||||
|
// aber pro Belegnummer und ohne Abschluss-Join. `bool_or` aggregiert
|
||||||
|
// über evtl. mehrere Lieferungen gleicher Belegnummer (true, sobald eine
|
||||||
|
// verändert ist) und liefert NULL, wenn es die Belegnummer nicht gibt
|
||||||
|
// ⇒ das mappt sqlx auf `None` (⇒ 404 im Handler).
|
||||||
|
let modified: Option<bool> = sqlx::query_scalar(
|
||||||
|
r#"
|
||||||
|
SELECT bool_or(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM delivery_items di
|
||||||
|
WHERE di.delivery_id = d.id
|
||||||
|
AND di.credited_quantity > 0
|
||||||
|
)
|
||||||
|
OR COALESCE(
|
||||||
|
( SELECT dca.action
|
||||||
|
FROM delivery_credit_audit dca
|
||||||
|
WHERE dca.delivery_id = d.id
|
||||||
|
ORDER BY dca.recorded_at DESC, dca.id DESC
|
||||||
|
LIMIT 1 ) = 'set',
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
FROM deliveries d
|
||||||
|
WHERE d.erp_belegnummer = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(belegnummer)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(db)?;
|
||||||
|
|
||||||
|
Ok(modified)
|
||||||
|
}
|
||||||
|
|
||||||
async fn mark_mail_sent(
|
async fn mark_mail_sent(
|
||||||
&self,
|
&self,
|
||||||
belegnummern: &[String],
|
belegnummern: &[String],
|
||||||
|
|||||||
Reference in New Issue
Block a user