feat(zahlung): Zahlungsabwicklung als eigenes Protokoll (POST /deliveries/{id}/payment)
- Neue Tabelle delivery_payments (append-only, idempotent ueber client_event_id): Methode + Code-Snapshot, server-seitig berechneter offener Betrag, Fahrer, Fahrzeug, Zeitpunkt - Endpoint prueft unter Zeilen-Lock: Lieferung aktiv, Methode aktiv, offener Betrag > 0 und identisch mit dem vom Fahrer bestaetigten Betrag - Offener Betrag als gemeinsamer Helper (open_amount_cents) fuer Zahlungsprotokoll und Abschluss - Abschluss-Gate: gueltige protokollierte Zahlung erfuellt die Inkasso-Pflicht und liefert die Methode; delivery_completions.payment_id verknuepft den Abschluss mit der Zahlung. Altes payment_collected-Flag bleibt fuer aeltere App-Versionen gueltig - Tour-Aggregat und Admin-Belegdetails liefern die juengste Zahlung - Einzel-Reset loescht auch das Zahlungsprotokoll - Integrationstest (ignored, braucht Wegwerf-DB) fuer Protokoll + Gate Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -27,10 +27,12 @@ use holzleitner_application::error::ApplicationError;
|
||||
use holzleitner_application::ports::TourRepository;
|
||||
use holzleitner_domain::{
|
||||
Address, Article, ContactChannel, ContactKind, ContactRole, ContactSource, Customer,
|
||||
CustomerContact, Delivery, DeliveryCredit, DeliveryItem, DeliveryNote, DeliveryServiceValue,
|
||||
DeliveryState, ScanState, ScanStatus, Service, ServiceKind, Tour, Warehouse,
|
||||
CustomerContact, Delivery, DeliveryCredit, DeliveryItem, DeliveryNote, DeliveryPayment,
|
||||
DeliveryServiceValue, DeliveryState, ScanState, ScanStatus, Service, ServiceKind, Tour, Warehouse,
|
||||
};
|
||||
|
||||
use super::delivery_payment_repository::{PAYMENT_COLUMNS, PaymentRow};
|
||||
|
||||
pub struct PgTourRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
@ -681,6 +683,23 @@ impl TourRepository for PgTourRepository {
|
||||
.filter_map(map_credit)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 8b. Jüngste protokollierte Zahlungsabwicklung pro Lieferung.
|
||||
let payments = sqlx::query_as::<_, PaymentRow>(&format!(
|
||||
r#"
|
||||
SELECT DISTINCT ON (delivery_id) {PAYMENT_COLUMNS}
|
||||
FROM delivery_payments
|
||||
WHERE delivery_id = ANY($1)
|
||||
ORDER BY delivery_id, recorded_at DESC, id DESC
|
||||
"#
|
||||
))
|
||||
.bind(&delivery_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(db)?
|
||||
.into_iter()
|
||||
.map(DeliveryPayment::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 9. Aktive Service-Definitionen (Stammdaten) — die App rendert daraus
|
||||
// Phase 4.
|
||||
let services = sqlx::query_as::<_, ServiceRow>(
|
||||
@ -764,6 +783,7 @@ impl TourRepository for PgTourRepository {
|
||||
warehouses,
|
||||
notes,
|
||||
credits,
|
||||
payments,
|
||||
services,
|
||||
delivery_services,
|
||||
contact_sources,
|
||||
@ -904,7 +924,8 @@ impl TourRepository for PgTourRepository {
|
||||
|
||||
async fn delete_all_tours(&self) -> Result<u64, ApplicationError> {
|
||||
// DELETE FROM tours cascadet per FK auf deliveries → delivery_items →
|
||||
// scan_audit, delivery_notes, delivery_credit_audit, delivery_services,
|
||||
// scan_audit, delivery_notes, delivery_credit_audit, delivery_payments,
|
||||
// delivery_services,
|
||||
// delivery_completions, attachments, delivery_contact_persons.
|
||||
let res = sqlx::query("DELETE FROM tours")
|
||||
.execute(&self.pool)
|
||||
@ -951,6 +972,15 @@ impl TourRepository for PgTourRepository {
|
||||
.await
|
||||
.map_err(db)?;
|
||||
|
||||
// Nach den Abschlüssen (FK payment_id), vor dem Status-Reset.
|
||||
sqlx::query(&format!(
|
||||
"DELETE FROM delivery_payments WHERE delivery_id IN ({BY_BELEG})"
|
||||
))
|
||||
.bind(belegnummer)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(db)?;
|
||||
|
||||
sqlx::query(&format!(
|
||||
"DELETE FROM delivery_credit_audit WHERE delivery_id IN ({BY_BELEG})"
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user