From d9bb316c4e05d65311fc6b381e8c80c1cde48afd Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Thu, 27 Nov 2025 20:10:34 +0530 Subject: [PATCH 1/4] Fix payment duplicate confirmation to return proper error --- app/eventyay/api/views/order.py | 4 +++- app/eventyay/base/models/orders.py | 10 ++++------ app/eventyay/base/payment.py | 7 +++++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/eventyay/api/views/order.py b/app/eventyay/api/views/order.py index ed2491d49e..39cc83fd60 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 @@ -1238,6 +1238,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..2f844f3b76 100644 --- a/app/eventyay/base/models/orders.py +++ b/app/eventyay/base/models/orders.py @@ -1664,17 +1664,15 @@ def confirm( generate_invoice, invoice_qualified, ) + 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 - logger.info( - 'Confirmed payment {} but ignored due to likely race condition.'.format( - self.full_id, - ) + # Payment is already confirmed, raise exception + raise PaymentAlreadyConfirmedException( + 'Payment {} has already been confirmed.'.format(self.full_id) ) - 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 From a9f578f41cf29ad012f53578953df31e93fddac7 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Fri, 28 Nov 2025 16:49:11 +0530 Subject: [PATCH 2/4] - logging, f-string, circular import clarification - API consistency for 409 responses --- app/eventyay/api/views/order.py | 6 ++++++ app/eventyay/base/models/orders.py | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/eventyay/api/views/order.py b/app/eventyay/api/views/order.py index 39cc83fd60..9a94888ee4 100644 --- a/app/eventyay/api/views/order.py +++ b/app/eventyay/api/views/order.py @@ -1221,6 +1221,12 @@ def confirm(self, request, **kwargs): force = request.data.get('force', False) send_mail = request.data.get('send_email', True) + if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED: + return Response( + {'detail': f'Payment {payment.full_id} has already been confirmed.'}, + status=status.HTTP_409_CONFLICT, + ) + if payment.state not in ( OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED, diff --git a/app/eventyay/base/models/orders.py b/app/eventyay/base/models/orders.py index 2f844f3b76..f455cec48d 100644 --- a/app/eventyay/base/models/orders.py +++ b/app/eventyay/base/models/orders.py @@ -1664,14 +1664,19 @@ 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: - # Payment is already confirmed, raise exception + # Payment is already confirmed; log and raise exception to preserve observability of race conditions + logger.info( + "Concurrent confirm attempt for already confirmed payment %s", + self.full_id, + ) raise PaymentAlreadyConfirmedException( - 'Payment {} has already been confirmed.'.format(self.full_id) + f'Payment {self.full_id} has already been confirmed.' ) locked_instance.state = self.PAYMENT_STATE_CONFIRMED From e8fdcc9b17e497d0abe81eb1ddce75a8a2f531d2 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Fri, 28 Nov 2025 17:00:57 +0530 Subject: [PATCH 3/4] Update app/eventyay/api/views/order.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/eventyay/api/views/order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/eventyay/api/views/order.py b/app/eventyay/api/views/order.py index 9a94888ee4..a4299036ba 100644 --- a/app/eventyay/api/views/order.py +++ b/app/eventyay/api/views/order.py @@ -1226,7 +1226,6 @@ def confirm(self, request, **kwargs): {'detail': f'Payment {payment.full_id} has already been confirmed.'}, status=status.HTTP_409_CONFLICT, ) - if payment.state not in ( OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED, From 1bf1a5b0d41ead283e27566c4ab72b56bd8a3511 Mon Sep 17 00:00:00 2001 From: Arnav Angarkar Date: Fri, 28 Nov 2025 17:02:51 +0530 Subject: [PATCH 4/4] reviewed changes --- app/eventyay/api/views/order.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/eventyay/api/views/order.py b/app/eventyay/api/views/order.py index a4299036ba..75608bb55d 100644 --- a/app/eventyay/api/views/order.py +++ b/app/eventyay/api/views/order.py @@ -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: @@ -1221,14 +1223,10 @@ def confirm(self, request, **kwargs): force = request.data.get('force', False) send_mail = request.data.get('send_email', True) - if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED: - return Response( - {'detail': f'Payment {payment.full_id} has already been confirmed.'}, - status=status.HTTP_409_CONFLICT, - ) if payment.state not in ( OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED, + OrderPayment.PAYMENT_STATE_CONFIRMED, ): return Response( {'detail': 'Invalid state of payment'},