feat(admin): GET /admin/belege/{belegnummer} - volle Lieferdetails
Neuer Admin-Endpunkt, der zu einer ERP-Belegnummer das komplette Detail-Paket einer Lieferung liefert (Kopf, Positionen mit Scan-Ständen, Kunde + Ansprechpartner, referenzierte Artikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte, Kontakte). Jede Lieferung, unabhaengig vom Status; 404 bei unbekannter Belegnummer. Umsetzung: TourRepository::find_tour_id_by_belegnummer loest die Belegnummer auf ihre tour_id auf; der Use Case laedt das bestehende Tour-Aggregat und destilliert daraus in-memory die eine Lieferung samt referenzierter Stammdaten (neues DTO DeliveryDetails). Kein neuer grosser Query. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
46
crates/application/src/dto/delivery_details.rs
Normal file
46
crates/application/src/dto/delivery_details.rs
Normal file
@ -0,0 +1,46 @@
|
||||
//! Detail-Antwort für `GET /admin/belege/{belegnummer}` — das volle Paket zu
|
||||
//! **einer** Lieferung, unabhängig vom Status.
|
||||
//!
|
||||
//! Gleiche Idee wie [`crate::dto::TourDetails`] (Stammdaten als deduplizierte
|
||||
//! Lookup-Listen, Client joint per Id), aber auf genau die eine Lieferung
|
||||
//! gefiltert. Wird im Use Case aus dem Tour-Aggregat destilliert.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use holzleitner_domain::{
|
||||
Article, ContactChannel, ContactSource, Customer, CustomerContact, DeliveryCredit,
|
||||
DeliveryNote, DeliveryServiceValue, Service, Tour, Warehouse,
|
||||
};
|
||||
|
||||
use super::tour_details::DeliveryWithItems;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeliveryDetails {
|
||||
/// Die Tour, zu der die Lieferung gehört.
|
||||
pub tour: Tour,
|
||||
/// Die Lieferung selbst inkl. Positionen (mit Scan-Ständen) und `sortOrder`.
|
||||
pub delivery: DeliveryWithItems,
|
||||
/// Der Kunde der Lieferung (`null`, falls unauffindbar — sollte nicht
|
||||
/// vorkommen).
|
||||
pub customer: Option<Customer>,
|
||||
/// Ansprechpartner des Kunden.
|
||||
pub customer_contacts: Vec<CustomerContact>,
|
||||
/// Nur die von den Positionen referenzierten Artikel.
|
||||
pub articles: Vec<Article>,
|
||||
/// Nur die von den Positionen referenzierten Lager.
|
||||
pub warehouses: Vec<Warehouse>,
|
||||
/// Notizen der Lieferung, aufsteigend nach `createdAt`.
|
||||
pub notes: Vec<DeliveryNote>,
|
||||
/// Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv).
|
||||
pub credit: Option<DeliveryCredit>,
|
||||
/// Aktive Service-Definitionen (Stammdaten).
|
||||
pub services: Vec<Service>,
|
||||
/// Für diese Lieferung gesetzte Service-Werte.
|
||||
pub delivery_services: Vec<DeliveryServiceValue>,
|
||||
/// Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner …).
|
||||
pub contact_sources: Vec<ContactSource>,
|
||||
/// Die zu `contactSources` gehörenden Einzel-Kanäle. Join per `sourceId`.
|
||||
pub contact_channels: Vec<ContactChannel>,
|
||||
}
|
||||
@ -14,6 +14,7 @@ pub mod car;
|
||||
pub mod complete;
|
||||
pub mod credit;
|
||||
pub mod delivery_action;
|
||||
pub mod delivery_details;
|
||||
pub mod delivery_report;
|
||||
pub mod delivery_order;
|
||||
pub mod note;
|
||||
@ -34,6 +35,7 @@ pub use delivery_report::{
|
||||
ReportItem, ReportNote, ReportScanAudit, ReportService,
|
||||
};
|
||||
pub use delivery_action::{CancelDeliveryRequest, DeliveryResponse, HoldDeliveryRequest};
|
||||
pub use delivery_details::DeliveryDetails;
|
||||
pub use delivery_order::{
|
||||
DeliveryOrderEntry, SetDeliveryOrderRequest, SetDeliveryOrderResponse,
|
||||
};
|
||||
|
||||
@ -31,6 +31,16 @@ pub trait TourRepository: Send + Sync {
|
||||
tour_id: Uuid,
|
||||
) -> Result<Option<TourDetails>, ApplicationError>;
|
||||
|
||||
/// Löst eine ERP-Belegnummer auf die `tour_id` ihrer Lieferung auf.
|
||||
/// `None`, wenn die Belegnummer keiner Lieferung zugeordnet ist. Bei
|
||||
/// mehreren Lieferungen gleicher Belegnummer wird eine (die erste)
|
||||
/// zurückgegeben. Dient dem Detail-Abruf per Belegnummer, der darüber das
|
||||
/// Tour-Aggregat lädt und auf die eine Lieferung filtert.
|
||||
async fn find_tour_id_by_belegnummer(
|
||||
&self,
|
||||
belegnummer: &str,
|
||||
) -> Result<Option<Uuid>, ApplicationError>;
|
||||
|
||||
/// Legt eine Tour samt Lieferungen und Positionen idempotent an
|
||||
/// bzw. aktualisiert sie. Gibt die `tour_id` zurück.
|
||||
async fn upsert_from_sync(
|
||||
|
||||
117
crates/application/src/usecases/get_delivery_details.rs
Normal file
117
crates/application/src/usecases/get_delivery_details.rs
Normal file
@ -0,0 +1,117 @@
|
||||
//! Use Case: volles Detail-Paket zu einer Lieferung per ERP-Belegnummer.
|
||||
//!
|
||||
//! Löst die Belegnummer auf ihre `tour_id` auf, lädt das (getestete)
|
||||
//! Tour-Aggregat und **destilliert** daraus genau die eine Lieferung samt der
|
||||
//! von ihr referenzierten Stammdaten. Kein neuer großer Query — reine
|
||||
//! In-Memory-Projektion des vorhandenen Aggregats. Status der Lieferung egal.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::{DeliveryDetails, TourDetails};
|
||||
use crate::error::ApplicationError;
|
||||
use crate::ports::TourRepository;
|
||||
|
||||
pub struct GetDeliveryDetailsUseCase {
|
||||
tours: Arc<dyn TourRepository>,
|
||||
}
|
||||
|
||||
impl GetDeliveryDetailsUseCase {
|
||||
pub fn new(tours: Arc<dyn TourRepository>) -> Self {
|
||||
Self { tours }
|
||||
}
|
||||
|
||||
/// `NotFound`, wenn die Belegnummer keiner Lieferung zugeordnet ist.
|
||||
pub async fn execute(
|
||||
&self,
|
||||
belegnummer: &str,
|
||||
) -> Result<DeliveryDetails, ApplicationError> {
|
||||
let tour_id = self
|
||||
.tours
|
||||
.find_tour_id_by_belegnummer(belegnummer)
|
||||
.await?
|
||||
.ok_or(ApplicationError::NotFound)?;
|
||||
|
||||
let TourDetails {
|
||||
tour,
|
||||
deliveries,
|
||||
customers,
|
||||
customer_contacts,
|
||||
articles,
|
||||
warehouses,
|
||||
notes,
|
||||
credits,
|
||||
services,
|
||||
delivery_services,
|
||||
contact_sources,
|
||||
contact_channels,
|
||||
} = self
|
||||
.tours
|
||||
.find_details_by_id(tour_id)
|
||||
.await?
|
||||
.ok_or(ApplicationError::NotFound)?;
|
||||
|
||||
// Die eine Lieferung aus dem Aggregat herauslösen.
|
||||
let delivery = deliveries
|
||||
.into_iter()
|
||||
.find(|d| d.delivery.erp_belegnummer == belegnummer)
|
||||
.ok_or(ApplicationError::NotFound)?;
|
||||
|
||||
let delivery_id = delivery.delivery.id;
|
||||
let customer_id = delivery.delivery.customer_id;
|
||||
let article_ids: HashSet<Uuid> =
|
||||
delivery.items.iter().map(|i| i.article_id).collect();
|
||||
let warehouse_ids: HashSet<Uuid> =
|
||||
delivery.items.iter().map(|i| i.warehouse_id).collect();
|
||||
|
||||
// Stammdaten auf das Referenzierte, Kollektionen auf die Lieferung filtern.
|
||||
let customer = customers.into_iter().find(|c| c.id == customer_id);
|
||||
let customer_contacts = customer_contacts
|
||||
.into_iter()
|
||||
.filter(|c| c.customer_id == customer_id)
|
||||
.collect();
|
||||
let articles = articles
|
||||
.into_iter()
|
||||
.filter(|a| article_ids.contains(&a.id))
|
||||
.collect();
|
||||
let warehouses = warehouses
|
||||
.into_iter()
|
||||
.filter(|w| warehouse_ids.contains(&w.id))
|
||||
.collect();
|
||||
let notes = notes
|
||||
.into_iter()
|
||||
.filter(|n| n.delivery_id == delivery_id)
|
||||
.collect();
|
||||
let credit = credits.into_iter().find(|c| c.delivery_id == delivery_id);
|
||||
let delivery_services = delivery_services
|
||||
.into_iter()
|
||||
.filter(|s| s.delivery_id == delivery_id)
|
||||
.collect();
|
||||
let contact_sources: Vec<_> = contact_sources
|
||||
.into_iter()
|
||||
.filter(|s| s.delivery_id == delivery_id)
|
||||
.collect();
|
||||
let source_ids: HashSet<Uuid> = contact_sources.iter().map(|s| s.id).collect();
|
||||
let contact_channels = contact_channels
|
||||
.into_iter()
|
||||
.filter(|c| source_ids.contains(&c.source_id))
|
||||
.collect();
|
||||
|
||||
Ok(DeliveryDetails {
|
||||
tour,
|
||||
delivery,
|
||||
customer,
|
||||
customer_contacts,
|
||||
articles,
|
||||
warehouses,
|
||||
notes,
|
||||
credit,
|
||||
services,
|
||||
delivery_services,
|
||||
contact_sources,
|
||||
contact_channels,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,7 @@ pub mod get_account;
|
||||
pub mod get_attachment_preview;
|
||||
pub mod get_tour;
|
||||
pub mod import_erp_tours;
|
||||
pub mod get_delivery_details;
|
||||
pub mod get_positions_modified;
|
||||
pub mod list_completed_deliveries;
|
||||
pub mod list_delivered_belegnummern;
|
||||
@ -48,6 +49,7 @@ pub use generate_delivery_report::GenerateDeliveryReportUseCase;
|
||||
pub use delete_delivery_note::DeleteDeliveryNoteUseCase;
|
||||
pub use get_account::GetAccountUseCase;
|
||||
pub use get_attachment_preview::GetAttachmentPreviewUseCase;
|
||||
pub use get_delivery_details::GetDeliveryDetailsUseCase;
|
||||
pub use get_positions_modified::GetPositionsModifiedUseCase;
|
||||
pub use get_tour::GetTourUseCase;
|
||||
pub use import_erp_tours::{ImportErpToursUseCase, ImportSummary};
|
||||
|
||||
Reference in New Issue
Block a user