Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/eventyay/api/views/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1192,6 +1192,8 @@ def create(self, request, *args, **kwargs):
force=request.data.get('force', False),
send_mail=send_mail,
)
except PaymentAlreadyConfirmedException:
Copy link
Member Author

@ArnavBallinCode ArnavBallinCode Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In create() flow: When creating a new payment and auto-confirming it, if another process already confirmed it, we want to silently succeed (payment exists and is confirmed -- okay )

In confirm() endpoint: We return HTTP 409 to tell the caller "already done"

This is intentional different behavior based on context

pass
except Quota.QuotaExceededException:
pass
except SendMailException:
Expand Down Expand Up @@ -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'},
Expand All @@ -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:
Expand Down
13 changes: 8 additions & 5 deletions app/eventyay/base/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions app/eventyay/base/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down