diff --git a/app/eventyay/api/views/order.py b/app/eventyay/api/views/order.py index ed2491d49e..75608bb55d 100644 --- a/app/eventyay/api/views/order.py +++ b/app/eventyay/api/views/order.py @@ -61,7 +61,7 @@ generate_secret, ) from eventyay.base.models.orders import QuestionAnswer, RevokedTicketSecret -from eventyay.base.payment import PaymentException +from eventyay.base.payment import PaymentException, PaymentAlreadyConfirmedException from eventyay.base.pdf import get_images from eventyay.base.secrets import assign_ticket_secret from eventyay.base.services import tickets @@ -1192,6 +1192,8 @@ def create(self, request, *args, **kwargs): force=request.data.get('force', False), send_mail=send_mail, ) + except PaymentAlreadyConfirmedException: + pass except Quota.QuotaExceededException: pass except SendMailException: @@ -1224,6 +1226,7 @@ def confirm(self, request, **kwargs): if payment.state not in ( OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED, + OrderPayment.PAYMENT_STATE_CONFIRMED, ): return Response( {'detail': 'Invalid state of payment'}, @@ -1238,6 +1241,8 @@ def confirm(self, request, **kwargs): send_mail=send_mail, force=force, ) + except PaymentAlreadyConfirmedException as e: + return Response({'detail': str(e)}, status=status.HTTP_409_CONFLICT) except Quota.QuotaExceededException as e: return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) except PaymentException as e: diff --git a/app/eventyay/base/models/orders.py b/app/eventyay/base/models/orders.py index dd45617c92..f455cec48d 100644 --- a/app/eventyay/base/models/orders.py +++ b/app/eventyay/base/models/orders.py @@ -1664,17 +1664,20 @@ def confirm( generate_invoice, invoice_qualified, ) + # Import here to avoid circular import (payment.py imports from models) + from eventyay.base.payment import PaymentAlreadyConfirmedException with transaction.atomic(): 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 + # Payment is already confirmed; log and raise exception to preserve observability of race conditions logger.info( - 'Confirmed payment {} but ignored due to likely race condition.'.format( - self.full_id, - ) + "Concurrent confirm attempt for already confirmed payment %s", + self.full_id, + ) + raise PaymentAlreadyConfirmedException( + f'Payment {self.full_id} has already been confirmed.' ) - return locked_instance.state = self.PAYMENT_STATE_CONFIRMED locked_instance.payment_date = payment_date or now() diff --git a/app/eventyay/base/payment.py b/app/eventyay/base/payment.py index df287394fd..e474e80f43 100644 --- a/app/eventyay/base/payment.py +++ b/app/eventyay/base/payment.py @@ -905,6 +905,13 @@ class PaymentException(Exception): # NOQA: N818 pass +class PaymentAlreadyConfirmedException(PaymentException): + """ + Raised when attempting to confirm a payment that has already been confirmed. + """ + pass + + class FreeOrderProvider(BasePaymentProvider): is_implicit = True is_enabled = True