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:
@ -6,8 +6,9 @@ use axum::routing::{patch, post, put};
|
||||
use holzleitner_application::dto::{
|
||||
AssignCarRequest, CancelDeliveryRequest, CompleteDeliveryAcknowledgements,
|
||||
CreateDeliveryNoteRequest, DeliveryCreditEventRequest, DeliveryCreditResponse,
|
||||
DeliveryNoteResponse, DeliveryResponse, DeliveryServiceResponse, HoldDeliveryRequest,
|
||||
SetDeliveryServiceRequest, UpdateDeliveryNoteRequest,
|
||||
DeliveryNoteResponse, DeliveryPaymentResponse, DeliveryResponse, DeliveryServiceResponse,
|
||||
HoldDeliveryRequest, RecordDeliveryPaymentRequest, SetDeliveryServiceRequest,
|
||||
UpdateDeliveryNoteRequest,
|
||||
};
|
||||
use holzleitner_application::error::ApplicationError;
|
||||
use holzleitner_application::ports::DeliveryAction;
|
||||
@ -39,6 +40,7 @@ pub fn router() -> Router<AppState> {
|
||||
.route("/deliveries/{delivery_id}/hold", post(hold))
|
||||
.route("/deliveries/{delivery_id}/resume", post(resume))
|
||||
.route("/deliveries/{delivery_id}/cancel", post(cancel))
|
||||
.route("/deliveries/{delivery_id}/payment", post(record_payment))
|
||||
.route("/deliveries/{delivery_id}/notes", post(create_note))
|
||||
.route(
|
||||
"/deliveries/{delivery_id}/notes/{note_id}",
|
||||
@ -475,6 +477,44 @@ pub async fn apply_credit(
|
||||
Ok(Json(DeliveryCreditResponse { credit }))
|
||||
}
|
||||
|
||||
/// Protokolliert die Zahlungsabwicklung („Abkassieren") einer Lieferung.
|
||||
/// Append-only, idempotent über `clientEventId`. Der Server berechnet den
|
||||
/// offenen Betrag selbst; er muss > 0 sein und `expectedAmountCents`
|
||||
/// entsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/deliveries/{delivery_id}/payment",
|
||||
tag = "deliveries",
|
||||
params(("delivery_id" = Uuid, Path)),
|
||||
request_body = RecordDeliveryPaymentRequest,
|
||||
responses(
|
||||
(status = 200, description = "Zahlung protokolliert", body = DeliveryPaymentResponse),
|
||||
(status = 400, description = "Lieferung nicht aktiv, Methode ungültig, kein offener Betrag oder Betrag abweichend"),
|
||||
(status = 401, description = "Authentifizierung fehlgeschlagen"),
|
||||
(status = 404, description = "Lieferung nicht gefunden")
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn record_payment(
|
||||
State(state): State<AppState>,
|
||||
AuthenticatedUser(claims): AuthenticatedUser,
|
||||
Path(delivery_id): Path<Uuid>,
|
||||
Json(req): Json<RecordDeliveryPaymentRequest>,
|
||||
) -> Result<Json<DeliveryPaymentResponse>, ApiError> {
|
||||
tracing::info!(
|
||||
actor = claims.personalnummer,
|
||||
%delivery_id,
|
||||
payment_method_id = %req.payment_method_id,
|
||||
expected_amount_cents = req.expected_amount_cents,
|
||||
"delivery.record_payment"
|
||||
);
|
||||
let payment = state
|
||||
.record_delivery_payment
|
||||
.execute(delivery_id, claims.personalnummer, req)
|
||||
.await?;
|
||||
Ok(Json(DeliveryPaymentResponse { payment }))
|
||||
}
|
||||
|
||||
/// Setzt (Upsert) den Wert eines Service für eine Lieferung. Genau das zum
|
||||
/// Service-Typ passende Feld (`boolValue`/`numericValue`) muss gesetzt sein;
|
||||
/// numerische Werte werden gegen min/max geprüft. Nur bei aktiver Lieferung.
|
||||
|
||||
Reference in New Issue
Block a user