Skip to content
Open
10 changes: 9 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 @@ -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,
Expand All @@ -1238,6 +1244,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