From 4d9dfa88fedf1f7b976670902b7c43a1ba84c2a1 Mon Sep 17 00:00:00 2001 From: Lukas Bockstaller Date: Thu, 6 Aug 2026 15:06:20 +0200 Subject: [PATCH] Paypal2: handle incomming capture webhooks (Z#23240966) (#6456) * store the state of the payment regardless of the state control.html shows the banner that the payment is in review depending on payment.info * handle capture ressource * Update src/pretix/plugins/paypal2/views.py Co-authored-by: Phin Wolkwitz * add test * cleanup logic regarding uninteresting resource_type * store payment.info during _execute_payment asap --------- Co-authored-by: Phin Wolkwitz --- src/pretix/plugins/paypal2/payment.py | 2 ++ src/pretix/plugins/paypal2/views.py | 11 ++++++----- src/tests/plugins/paypal2/test_webhook.py | 6 ++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/pretix/plugins/paypal2/payment.py b/src/pretix/plugins/paypal2/payment.py index c279ce2352..84f3edf605 100644 --- a/src/pretix/plugins/paypal2/payment.py +++ b/src/pretix/plugins/paypal2/payment.py @@ -677,6 +677,8 @@ class PaypalMethod(BasePaymentProvider): raise PaymentException(_('We had trouble communicating with PayPal')) else: pp_captured_order = response.result + payment.info = json.dumps(pp_captured_order.dict()) + payment.save() try: ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id) diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py index 56b118230d..37340e22a4 100644 --- a/src/pretix/plugins/paypal2/views.py +++ b/src/pretix/plugins/paypal2/views.py @@ -357,14 +357,13 @@ def webhook(request, *args, **kwargs): if 'resource_type' not in event_json: return HttpResponse("Invalid body, no resource_type given", status=400) - if event_json['resource_type'] not in ["checkout-order", "refund", "capture"]: - return HttpResponse("Not interested in this resource type", status=200) - # Retrieve the Charge ID of the refunded payment - if event_json['resource_type'] == 'refund': + if event_json['resource_type'] == 'checkout-order': + payloadid = event_json['resource']['id'] + elif event_json['resource_type'] == 'refund' or event_json['resource_type'] == 'capture': payloadid = get_link(event_json['resource']['links'], 'up')['href'].split('/')[-1] else: - payloadid = event_json['resource']['id'] + return HttpResponse("Not interested in this resource type", status=200) refs = [payloadid] if event_json['resource'].get('supplementary_data', {}).get('related_ids', {}).get('order_id'): @@ -424,6 +423,8 @@ def webhook(request, *args, **kwargs): **event_json, '_order_state': sale.dict(), }) + payment.info = json.dumps(sale.dict()) + payment.save() if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'): if event_json['resource_type'] == 'refund': diff --git a/src/tests/plugins/paypal2/test_webhook.py b/src/tests/plugins/paypal2/test_webhook.py index b1355018c7..21eb888531 100644 --- a/src/tests/plugins/paypal2/test_webhook.py +++ b/src/tests/plugins/paypal2/test_webhook.py @@ -22,6 +22,7 @@ import json from datetime import timedelta from decimal import Decimal +from unittest.mock import MagicMock import pytest from django.utils.timezone import now @@ -409,7 +410,8 @@ def test_webhook_mark_paid(env, client, monkeypatch): order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING) pp_order = Result(get_test_order()) - monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order) + mock_orders_get_request = MagicMock(return_value=pp_order) + monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", mock_orders_get_request) monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api) with scopes_disabled(): ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(), @@ -497,7 +499,7 @@ def test_webhook_mark_paid(env, client, monkeypatch): "resource_version": "2.0" } ), content_type='application_json') - + mock_orders_get_request.assert_called_once_with('806440346Y391300T') order.refresh_from_db() assert order.status == Order.STATUS_PAID