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) locked_instance = OrderPayment.objects.select_for_update().get(pk=self.pk)
if locked_instance.state == self.PAYMENT_STATE_CONFIRMED: if locked_instance.state == self.PAYMENT_STATE_CONFIRMED:
# Race condition detected, this payment is already 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 return
locked_instance.state = self.PAYMENT_STATE_CONFIRMED locked_instance.state = self.PAYMENT_STATE_CONFIRMED
@@ -1305,6 +1308,7 @@ class OrderPayment(models.Model):
}, user=user, auth=auth) }, user=user, auth=auth)
if self.order.status in (Order.STATUS_PAID, Order.STATUS_CANCELED): 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 return
payment_sum = self.order.payments.filter( payment_sum = self.order.payments.filter(
@@ -1315,6 +1319,9 @@ class OrderPayment(models.Model):
OrderRefund.REFUND_STATE_CREATED) OrderRefund.REFUND_STATE_CREATED)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00') ).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
if payment_sum - refund_sum < self.order.total: 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 return
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock: 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. On failure, you should raise a PaymentException.
""" """
payment.state = OrderPayment.PAYMENT_STATE_CANCELED payment.state = OrderPayment.PAYMENT_STATE_CANCELED
payment.save() payment.save(update_fields=['state'])
def execute_refund(self, refund: OrderRefund): 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 import transaction
from django.db.models import Exists, F, Max, Min, OuterRef, Q, Sum from django.db.models import Exists, F, Max, Min, OuterRef, Q, Sum
from django.db.models.functions import Coalesce, Greatest from django.db.models.functions import Coalesce, Greatest
from django.db.transaction import get_connection
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now 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, def change_payment_provider(order: Order, payment_provider, amount=None, new_payment=None, create_log=True,
recreate_invoices=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 oldtotal = order.total
e = OrderPayment.objects.filter(fee=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, e = OrderPayment.objects.filter(fee=OuterRef('pk'), state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED,
OrderPayment.PAYMENT_STATE_REFUNDED)) 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( new_fee = payment_provider.calculate_fee(
order.pending_sum - old_fee if amount is None else amount order.pending_sum - old_fee if amount is None else amount
) )
with transaction.atomic(): if new_fee:
if new_fee: fee.value = new_fee
fee.value = new_fee fee.internal_type = payment_provider.identifier
fee.internal_type = payment_provider.identifier fee._calculate_tax()
fee._calculate_tax() fee.save()
fee.save() else:
else: if fee.pk:
if fee.pk: fee.delete()
fee.delete() fee = None
fee = None
open_payment = None open_payment = None
if new_payment: 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: else:
lp = order.payments.last() lp = order.payments.select_for_update().last()
if lp and lp.state not in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED):
if lp and lp.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED):
open_payment = lp open_payment = lp
if open_payment and open_payment.state in (OrderPayment.PAYMENT_STATE_PENDING, if open_payment:
OrderPayment.PAYMENT_STATE_CREATED):
try: try:
with transaction.atomic(): open_payment.payment_provider.cancel_payment(open_payment)
open_payment.payment_provider.cancel_payment(open_payment) order.log_action('pretix.event.order.payment.canceled', {
order.log_action('pretix.event.order.payment.canceled', { 'local_id': open_payment.local_id,
'local_id': open_payment.local_id, 'provider': open_payment.provider,
'provider': open_payment.provider, })
})
except PaymentException as e: except PaymentException as e:
order.log_action( order.log_action(
'pretix.event.order.payment.canceled.failed', 'pretix.event.order.payment.canceled.failed',