From 94f1511abbeb8f14241b716cfe98447b1135a826 Mon Sep 17 00:00:00 2001 From: Lukas Bockstaller Date: Tue, 11 Aug 2026 13:50:44 +0200 Subject: [PATCH] add timeout to paypal after which a pending payment can be canceled --- src/pretix/plugins/paypal2/payment.py | 61 +++++++++++++++++-- .../pretixplugins/paypal2/pending.html | 7 ++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/pretix/plugins/paypal2/payment.py b/src/pretix/plugins/paypal2/payment.py index 84d4a999e8..4ce6333141 100644 --- a/src/pretix/plugins/paypal2/payment.py +++ b/src/pretix/plugins/paypal2/payment.py @@ -23,7 +23,7 @@ import json import logging import urllib.parse from collections import OrderedDict -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from decimal import Decimal from django import forms @@ -55,7 +55,7 @@ from pretix.base.forms import SecretKeySettingsField from pretix.base.forms.questions import guess_country from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota from pretix.base.payment import BasePaymentProvider, PaymentException -from pretix.base.settings import SettingsSandbox +from pretix.base.settings import SettingsSandbox, settings_hierarkey from pretix.helpers import OF_SELF from pretix.helpers.urls import mainreverse_absolute from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute @@ -193,6 +193,31 @@ class PaypalSettingsHolder(BasePaymentProvider): } ) )), + ('allow_retries_during_compliance_hold', + forms.BooleanField( + label=_('Allow further payments during compliance hold'), + help_text=_( + 'PayPals fraud prevention might block processing of individual payments for a considerable amount ' + 'of time. The payment is marked as "pending" during this time window. You can allow your customers to ' + 'start another payment attempts during that window. This might result in overpayment of orders if the' + 'original payment is approved.' + ), + required=False + )), + ('timeout_payment_during_compliance_hold', + forms.IntegerField( + label=_('Timeout further payment attempts'), + help_text=_( + 'Time duration in minutes after which another payment attempt is possible, while the last payment is ' + 'still under investigation.' + ), + required=False, + widget=forms.NumberInput( + attrs={ + 'data-checkbox-dependency': '#id_payment_paypal_allow_retries_during_compliance_hold', + } + ) + )), ] @@ -517,6 +542,18 @@ class PaypalMethod(BasePaymentProvider): @property def abort_pending_allowed(self): + return True + + def abort_pending_payment_allowed(self, payment) -> bool: + if not self.settings.get('allow_retries_during_compliance_hold', as_type=bool, default=True): + return False + + if payment.info_data.get('create_time', False): + create_time = datetime.fromisoformat(payment.info_data['create_time']) + duration = self.settings.get('timeout_payment_during_compliance_hold', as_type=int, default=0) + if datetime.now(tz=timezone.utc) - create_time > timedelta(minutes=duration): + return True + return False def _create_paypal_order(self, request, payment=None, cart_total=None): @@ -859,22 +896,34 @@ class PaypalMethod(BasePaymentProvider): logger.info('{}: {} - paypal payment processing time'.format(str(payment.global_id), str(duration))) def payment_pending_render(self, request, payment) -> str: + stuck_in_compliance = False retry = True + + if payment.state == OrderPayment.PAYMENT_STATE_PENDING: + retry = self.abort_pending_payment_allowed(payment) and self.abort_pending_allowed + try: if ( payment.info and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'PENDING' ): - retry = False + stuck_in_compliance = True except (KeyError, IndexError): pass + try: + if payment.info and payment.info_data['status'] == "APPROVED": + stuck_in_compliance = True + except (KeyError): + pass + error = payment.info_data.get("error", {}) is_known_issue = error.get("name") == "RESOURCE_NOT_FOUND" or "RESOURCE_NOT_FOUND" in (error.get("message") or "") template = get_template('pretixplugins/paypal2/pending.html') ctx = {'request': request, 'event': self.event, 'settings': self.settings, - 'retry': retry, 'order': payment.order, 'is_known_issue': is_known_issue} + 'stuck_in_compliance': stuck_in_compliance, 'retry': retry, 'order': payment.order, + 'is_known_issue': is_known_issue} return template.render(ctx) def matching_id(self, payment: OrderPayment): @@ -1122,6 +1171,10 @@ class PaypalMethod(BasePaymentProvider): return self.settings.get('_invoice_text', as_type=LazyI18nString, default='') +settings_hierarkey.add_default('payment_paypal_allow_retries_during_compliance_hold', True, bool) +settings_hierarkey.add_default('payment_paypal_timeout_payment_during_compliance_hold', 0, int) + + class PaypalWallet(PaypalMethod): identifier = 'paypal' verbose_name = _('PayPal') diff --git a/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html b/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html index 640bbd4bcc..24bfe6e683 100644 --- a/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html +++ b/src/pretix/plugins/paypal2/templates/pretixplugins/paypal2/pending.html @@ -6,9 +6,12 @@ Your payment has failed due to a known issue within PayPal. Please try again, there is a high chance of the payment succeeding on a second or third attempt. You can also try other payment methods, if available. {% endblocktrans %} - {% else %} +{% elif stuck_in_compliance %}

{% blocktrans trimmed %} - Our attempt to execute your payment via PayPal has failed. Please try again or contact us. + Your payment is being processed by PayPal. This takes longer than usual. You can wait until PayPal + acknowledges the payment or you can try paying again with this or another payment method. + This might result in you being charged twice in case PayPal allows your initial payment attempt. + Please contact us, to resolve this case. {% endblocktrans %}

{% endif %} {% else %}