Improved logging and transaction handling around payment confirmations

This commit is contained in:
Raphael Michel
2020-02-05 12:02:02 +01:00
parent eb11dac21e
commit da68cb618e
3 changed files with 31 additions and 22 deletions

View File

@@ -1285,6 +1285,9 @@ class OrderPayment(models.Model):
locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
# Race condition detected, this payment is already confirmed
logger.info('Confirmed payment {} but ignored due to likely race condition.'.format(
self.full_id,
))
return
locked_instance.state = self.PAYMENT_STATE_CONFIRMED
@@ -1305,6 +1308,7 @@ class OrderPayment(models.Model):
}, user=user, auth=auth)
if self.order.status in (Order.STATUS_PAID, Order.STATUS_CANCELED):
logger.info('Confirmed payment {} but order is in status {}.'.format(self.full_id, self.order.status))
return
payment_sum = self.order.payments.filter(
@@ -1315,6 +1319,9 @@ class OrderPayment(models.Model):
OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
if payment_sum - refund_sum < self.order.total:
logger.info('Confirmed payment {} but payment sum is {} and refund sum is.'.format(
self.full_id, payment_sum, refund_sum
))
return
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:

View File

@@ -687,7 +687,7 @@ class BasePaymentProvider:
On failure, you should raise a PaymentException.
"""
payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save()
payment.save(update_fields=['state'])
def execute_refund(self, refund: OrderRefund):
"""

View File

@@ -11,6 +11,7 @@ from django.core.cache import cache
from django.db import transaction
from django.db.models import Exists, F, Max, Min, OuterRef, Q, Sum
from django.db.models.functions import Coalesce, Greatest
from django.db.transaction import get_connection
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
@@ -1863,6 +1864,9 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None, create_log=True,
recreate_invoices=True):
if not get_connection().in_atomic_block:
raise Exception('change_payment_provider should only be called in atomic transaction!')
oldtotal = order.total
e = OrderPayment.objects.filter(fee=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_REFUNDED))
@@ -1883,34 +1887,32 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
new_fee = payment_provider.calculate_fee(
order.pending_sum - old_fee if amount is None else amount
)
with transaction.atomic():
if new_fee:
fee.value = new_fee
fee.internal_type = payment_provider.identifier
fee._calculate_tax()
fee.save()
else:
if fee.pk:
fee.delete()
fee = None
if new_fee:
fee.value = new_fee
fee.internal_type = payment_provider.identifier
fee._calculate_tax()
fee.save()
else:
if fee.pk:
fee.delete()
fee = None
open_payment = None
if new_payment:
lp = order.payments.exclude(pk=new_payment.pk).last()
lp = order.payments.select_for_update().exclude(pk=new_payment.pk).last()
else:
lp = order.payments.last()
if lp and lp.state not in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED):
lp = order.payments.select_for_update().last()
if lp and lp.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
open_payment = lp
if open_payment and open_payment.state in (OrderPayment.PAYMENT_STATE_PENDING,
OrderPayment.PAYMENT_STATE_CREATED):
if open_payment:
try:
with transaction.atomic():
open_payment.payment_provider.cancel_payment(open_payment)
order.log_action('pretix.event.order.payment.canceled', {
'local_id': open_payment.local_id,
'provider': open_payment.provider,
})
open_payment.payment_provider.cancel_payment(open_payment)
order.log_action('pretix.event.order.payment.canceled', {
'local_id': open_payment.local_id,
'provider': open_payment.provider,
})
except PaymentException as e:
order.log_action(
'pretix.event.order.payment.canceled.failed',