forked from CGM_Public/pretix_original
BasePaymentProvider & PayPal2: allow to cancel pending payments on a per payment basis (Z#23240966) (#6472)
* move payment into pending on PENDING_REVIEW webhook * mark approved payment as pending * extend BasePaymentProvider to gate aborting pending payments on a payment per payment basis * add timeout to paypal after which a pending payment can be canceled * formatting * add missing negation * cleanup abort_pending_allowed methods * Apply suggestions from code review Co-authored-by: pajowu <pajowu@pajowu.de> * check all capture elements * rename method and change defaults * remove left over Constant * flake8 . --------- Co-authored-by: pajowu <pajowu@pajowu.de>
This commit is contained in:
co-authored by
pajowu
parent
4e5fbacf6d
commit
c4a5a9a84d
@@ -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
|
||||
@@ -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 them being charged twice 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',
|
||||
}
|
||||
)
|
||||
)),
|
||||
|
||||
]
|
||||
|
||||
@@ -515,8 +540,16 @@ class PaypalMethod(BasePaymentProvider):
|
||||
'XPF': 0,
|
||||
}))
|
||||
|
||||
@property
|
||||
def abort_pending_allowed(self):
|
||||
def _payment_abort_pending_allowed(self, payment) -> bool:
|
||||
if not self.settings.get('allow_retries_during_compliance_hold', as_type=bool, default=False):
|
||||
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=10)
|
||||
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):
|
||||
@@ -678,6 +711,8 @@ class PaypalMethod(BasePaymentProvider):
|
||||
else:
|
||||
pp_captured_order = response.result
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
if pp_captured_order.status == 'APPROVED':
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
|
||||
try:
|
||||
@@ -857,14 +892,20 @@ class PaypalMethod(BasePaymentProvider):
|
||||
logger.info('{}: {} - paypal payment processing time'.format(str(payment.global_id), str(duration)))
|
||||
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
retry = True
|
||||
stuck_in_compliance = False
|
||||
retry = self._payment_abort_pending_allowed(payment)
|
||||
try:
|
||||
if (
|
||||
payment.info
|
||||
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'PENDING'
|
||||
):
|
||||
retry = False
|
||||
except (KeyError, IndexError):
|
||||
for purchase_unit in payment.info_data['purchase_units']:
|
||||
for capture in purchase_unit['payments']['captures']:
|
||||
if capture['status'] == "PENDING":
|
||||
stuck_in_compliance = True
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if payment.info_data.get('status') == "APPROVED":
|
||||
stuck_in_compliance = True
|
||||
except (KeyError):
|
||||
pass
|
||||
|
||||
error = payment.info_data.get("error", {})
|
||||
@@ -872,7 +913,8 @@ class PaypalMethod(BasePaymentProvider):
|
||||
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user