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:
@ -33,6 +33,7 @@ use axum::Router;
|
||||
use axum::middleware::from_fn_with_state;
|
||||
use holzleitner_application::usecases::{
|
||||
ApplyDeliveryActionUseCase, ApplyDeliveryCreditEventUseCase, ApplyScansUseCase,
|
||||
RecordDeliveryPaymentUseCase,
|
||||
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
|
||||
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
|
||||
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
||||
@ -64,7 +65,7 @@ use holzleitner_infrastructure::report::{
|
||||
};
|
||||
use holzleitner_infrastructure::persistence::{
|
||||
PgAccountRepository, PgAttachmentRepository, PgCarRepository, PgDeliveryCompletionRepository,
|
||||
PgDeliveryCreditRepository, PgDeliveryNoteRepository, PgDeliveryReportJobRepository,
|
||||
PgDeliveryCreditRepository, PgDeliveryNoteRepository, PgDeliveryPaymentRepository, PgDeliveryReportJobRepository,
|
||||
PgDeliveryRepository, PgDeliveryServiceRepository, PgPaymentMethodRepository, PgReviewRepository,
|
||||
PgScanRepository,
|
||||
PgServiceRepository, PgSyncRunRepository, PgTourRepository, PoolConfig, connect_and_migrate,
|
||||
@ -215,6 +216,7 @@ pub(crate) async fn run_app(
|
||||
let car_repository = Arc::new(PgCarRepository::new(pool.clone()));
|
||||
let payment_method_repository = Arc::new(PgPaymentMethodRepository::new(pool.clone()));
|
||||
let delivery_credit_repository = Arc::new(PgDeliveryCreditRepository::new(pool.clone()));
|
||||
let delivery_payment_repository = Arc::new(PgDeliveryPaymentRepository::new(pool.clone()));
|
||||
let delivery_completion_repository = Arc::new(PgDeliveryCompletionRepository::new(
|
||||
pool.clone(),
|
||||
cfg.server.timezone,
|
||||
@ -412,6 +414,10 @@ pub(crate) async fn run_app(
|
||||
delivery_credit_repository,
|
||||
car_repository.clone(),
|
||||
));
|
||||
let record_delivery_payment = Arc::new(RecordDeliveryPaymentUseCase::new(
|
||||
delivery_payment_repository,
|
||||
car_repository.clone(),
|
||||
));
|
||||
let create_delivery_note = Arc::new(CreateDeliveryNoteUseCase::new(
|
||||
delivery_note_repository.clone(),
|
||||
car_repository.clone(),
|
||||
@ -486,6 +492,7 @@ pub(crate) async fn run_app(
|
||||
list_pending_reviews,
|
||||
resolve_review,
|
||||
apply_delivery_credit_event,
|
||||
record_delivery_payment,
|
||||
create_delivery_note,
|
||||
update_delivery_note,
|
||||
delete_delivery_note,
|
||||
|
||||
@ -36,6 +36,7 @@ use utoipa::openapi::security::{
|
||||
crate::routes::deliveries::delete_note,
|
||||
crate::routes::deliveries::upload_note_image,
|
||||
crate::routes::deliveries::apply_credit,
|
||||
crate::routes::deliveries::record_payment,
|
||||
crate::routes::attachments::get_attachment,
|
||||
crate::routes::deliveries::assign_car,
|
||||
crate::routes::deliveries::set_service,
|
||||
@ -78,6 +79,7 @@ use utoipa::openapi::security::{
|
||||
holzleitner_domain::DeliveryItem,
|
||||
holzleitner_domain::DeliveryNote,
|
||||
holzleitner_domain::DeliveryCredit,
|
||||
holzleitner_domain::DeliveryPayment,
|
||||
holzleitner_domain::DeliveryState,
|
||||
holzleitner_domain::ScanState,
|
||||
holzleitner_domain::ScanStatus,
|
||||
@ -114,6 +116,8 @@ use utoipa::openapi::security::{
|
||||
holzleitner_application::dto::CreditAction,
|
||||
holzleitner_application::dto::DeliveryCreditEventRequest,
|
||||
holzleitner_application::dto::DeliveryCreditResponse,
|
||||
holzleitner_application::dto::RecordDeliveryPaymentRequest,
|
||||
holzleitner_application::dto::DeliveryPaymentResponse,
|
||||
holzleitner_application::dto::CreateCarRequest,
|
||||
holzleitner_application::dto::UpdateCarRequest,
|
||||
holzleitner_application::dto::CarResponse,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use holzleitner_application::ports::AuthService;
|
||||
use holzleitner_application::usecases::{
|
||||
ApplyDeliveryActionUseCase, ApplyDeliveryCreditEventUseCase, ApplyScansUseCase,
|
||||
RecordDeliveryPaymentUseCase,
|
||||
AssignCarToDeliveryUseCase, CompleteDeliveryUseCase, CreateDeliveryNoteUseCase,
|
||||
CreateMyCarUseCase, CreatePaymentMethodUseCase, CreateServiceUseCase,
|
||||
DeleteDeliveryNoteUseCase, DeleteDeliveryServiceUseCase, DeletePaymentMethodUseCase,
|
||||
@ -67,6 +68,7 @@ pub struct AppState {
|
||||
pub list_pending_reviews: Arc<ListPendingReviewsUseCase>,
|
||||
pub resolve_review: Arc<ResolveReviewUseCase>,
|
||||
pub apply_delivery_credit_event: Arc<ApplyDeliveryCreditEventUseCase>,
|
||||
pub record_delivery_payment: Arc<RecordDeliveryPaymentUseCase>,
|
||||
pub create_delivery_note: Arc<CreateDeliveryNoteUseCase>,
|
||||
pub update_delivery_note: Arc<UpdateDeliveryNoteUseCase>,
|
||||
pub delete_delivery_note: Arc<DeleteDeliveryNoteUseCase>,
|
||||
|
||||
Reference in New Issue
Block a user