From 25e651f3e8dfe6c161261686fae75ef8f37a860a Mon Sep 17 00:00:00 2001 From: Lukas Bockstaller Date: Mon, 17 Aug 2026 11:48:48 +0200 Subject: [PATCH] handle failed captures not stored in sale --- src/pretix/plugins/paypal2/views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py index b01a04f9cc..31c8b394cc 100644 --- a/src/pretix/plugins/paypal2/views.py +++ b/src/pretix/plugins/paypal2/views.py @@ -426,6 +426,22 @@ def webhook(request, *args, **kwargs): payment.info = json.dumps(sale.dict()) payment.save() + # the captures[] of the sales object only is populated if the capture request isn't rejected. + # the capture request might be rejected if certain validations aren't met OR if the payment is + # DECLINED, nevertheless we will get a webhook informing us about "PAYMENT.CAPTURE.DECLINED". + # With no trace of it in `sale`. + # So now we have to leave our current pattern of making only decisions based upon the + # complete payment object (and checking whenever we receive a webhook), and instead need to fail + # payment directly. + # Otherwise we are caught in a loop: + # 1. We get a webhook and get `sale` + # 2. We see no proof of a capture attempt in `sale` + # 3. We call execute_payment and trigger a new "PAYMENT.CAPTURE.DECLINED" webhook, GOTO 1 + if event_json['event_type'] == "PAYMENT.CAPTURE.DECLINED": + payment.fail(log_data={'status': event_json['event_type']}) + logger.exception('PayPal Webhook PAYMENT.CAPTURE.DECLINED: {}'.format(event_json)) + return HttpResponse(status=200) + if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'): if event_json['resource_type'] == 'refund': try: