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 <wolkwitz@pretix.eu>

* add test

* cleanup logic regarding uninteresting resource_type

* store payment.info during _execute_payment asap

---------

Co-authored-by: Phin Wolkwitz <wolkwitz@pretix.eu>
This commit is contained in:
Lukas Bockstaller
2026-08-06 15:06:20 +02:00
committed by GitHub
co-authored by Phin Wolkwitz
parent ea792e76b2
commit 4d9dfa88fe
3 changed files with 12 additions and 7 deletions
+2
View File
@@ -677,6 +677,8 @@ class PaypalMethod(BasePaymentProvider):
raise PaymentException(_('We had trouble communicating with PayPal')) raise PaymentException(_('We had trouble communicating with PayPal'))
else: else:
pp_captured_order = response.result pp_captured_order = response.result
payment.info = json.dumps(pp_captured_order.dict())
payment.save()
try: try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id) ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id)
+6 -5
View File
@@ -357,14 +357,13 @@ def webhook(request, *args, **kwargs):
if 'resource_type' not in event_json: if 'resource_type' not in event_json:
return HttpResponse("Invalid body, no resource_type given", status=400) 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 # 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] payloadid = get_link(event_json['resource']['links'], 'up')['href'].split('/')[-1]
else: else:
payloadid = event_json['resource']['id'] return HttpResponse("Not interested in this resource type", status=200)
refs = [payloadid] refs = [payloadid]
if event_json['resource'].get('supplementary_data', {}).get('related_ids', {}).get('order_id'): if event_json['resource'].get('supplementary_data', {}).get('related_ids', {}).get('order_id'):
@@ -424,6 +423,8 @@ def webhook(request, *args, **kwargs):
**event_json, **event_json,
'_order_state': sale.dict(), '_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 payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'):
if event_json['resource_type'] == 'refund': if event_json['resource_type'] == 'refund':
+4 -2
View File
@@ -22,6 +22,7 @@
import json import json
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from unittest.mock import MagicMock
import pytest import pytest
from django.utils.timezone import now 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) order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
pp_order = Result(get_test_order()) 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) monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
with scopes_disabled(): with scopes_disabled():
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(), 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" "resource_version": "2.0"
} }
), content_type='application_json') ), content_type='application_json')
mock_orders_get_request.assert_called_once_with('806440346Y391300T')
order.refresh_from_db() order.refresh_from_db()
assert order.status == Order.STATUS_PAID assert order.status == Order.STATUS_PAID