Make protocoin transactions atomic to fix race conditions

This commit is contained in:
Tanner Collin 2022-08-23 21:23:21 +00:00
parent 226008a0c2
commit 9bb80f6dce

View File

@ -1047,85 +1047,89 @@ class InterestViewSet(Base, Retrieve, Create):
class ProtocoinViewSet(Base): class ProtocoinViewSet(Base):
@action(detail=False, methods=['post'], permission_classes=[AllowMetadata | IsAuthenticated]) @action(detail=False, methods=['post'], permission_classes=[AllowMetadata | IsAuthenticated])
def send_to_member(self, request): def send_to_member(self, request):
source_user = self.request.user
source_member = source_user.member
try: try:
member_id = int(request.data['member_id']) with transaction.atomic():
except KeyError: source_user = self.request.user
raise exceptions.ValidationError(dict(member_id='This field is required.')) source_member = source_user.member
except ValueError:
raise exceptions.ValidationError(dict(member_id='Invalid number.'))
try: try:
balance = float(request.data['balance']) member_id = int(request.data['member_id'])
except KeyError: except KeyError:
raise exceptions.ValidationError(dict(balance='This field is required.')) raise exceptions.ValidationError(dict(member_id='This field is required.'))
except ValueError: except ValueError:
raise exceptions.ValidationError(dict(balance='Invalid number.')) raise exceptions.ValidationError(dict(member_id='Invalid number.'))
try: try:
amount = float(request.data['amount']) balance = float(request.data['balance'])
except KeyError: except KeyError:
raise exceptions.ValidationError(dict(amount='This field is required.')) raise exceptions.ValidationError(dict(balance='This field is required.'))
except ValueError: except ValueError:
raise exceptions.ValidationError(dict(amount='Invalid number.')) raise exceptions.ValidationError(dict(balance='Invalid number.'))
if amount < 1.00: try:
raise exceptions.ValidationError(dict(amount='Amount too small.')) amount = float(request.data['amount'])
except KeyError:
raise exceptions.ValidationError(dict(amount='This field is required.'))
except ValueError:
raise exceptions.ValidationError(dict(amount='Invalid number.'))
if amount < 1.00:
raise exceptions.ValidationError(dict(amount='Amount too small.'))
if member_id == source_member.id: if member_id == source_member.id:
raise exceptions.ValidationError(dict(member_id='Unable to send to self.')) raise exceptions.ValidationError(dict(member_id='Unable to send to self.'))
destination_member = get_object_or_404(models.Member, id=member_id) destination_member = get_object_or_404(models.Member, id=member_id)
destination_user = destination_member.user destination_user = destination_member.user
source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum']
source_user_balance = float(source_user_balance) source_user_balance = float(source_user_balance)
if source_user_balance != balance: if source_user_balance != balance:
raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) raise exceptions.ValidationError(dict(balance='Incorrect current balance.'))
if source_user_balance < amount: if source_user_balance < amount:
raise exceptions.ValidationError(dict(amount='Insufficient funds.')) raise exceptions.ValidationError(dict(amount='Insufficient funds.'))
source_delta = -amount source_delta = -amount
destination_delta = amount destination_delta = amount
memo = 'Protocoin - Transaction {} ({}) sent ₱ {} to {} ({})'.format( memo = 'Protocoin - Transaction {} ({}) sent ₱ {} to {} ({})'.format(
source_member.first_name + ' ' + source_member.last_name, source_member.first_name + ' ' + source_member.last_name,
source_member.id, source_member.id,
amount, amount,
destination_member.first_name + ' ' + destination_member.last_name, destination_member.first_name + ' ' + destination_member.last_name,
destination_member.id, destination_member.id,
) )
tx = models.Transaction.objects.create( tx = models.Transaction.objects.create(
user=source_user, user=source_user,
protocoin=source_delta, protocoin=source_delta,
amount=0, amount=0,
number_of_membership_months=0, number_of_membership_months=0,
account_type='Protocoin', account_type='Protocoin',
category='Other', category='Other',
info_source='System', info_source='System',
memo=memo, memo=memo,
) )
utils.log_transaction(tx) utils.log_transaction(tx)
tx = models.Transaction.objects.create( tx = models.Transaction.objects.create(
user=destination_user, user=destination_user,
protocoin=destination_delta, protocoin=destination_delta,
amount=0, amount=0,
number_of_membership_months=0, number_of_membership_months=0,
account_type='Protocoin', account_type='Protocoin',
category='Other', category='Other',
info_source='System', info_source='System',
memo=memo, memo=memo,
) )
utils.log_transaction(tx) utils.log_transaction(tx)
return Response(200) return Response(200)
except OperationalError:
self.send_to_member(request)
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def card_vend_balance(self, request, pk=None): def card_vend_balance(self, request, pk=None):
@ -1147,65 +1151,69 @@ class ProtocoinViewSet(Base):
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def card_vend_request(self, request, pk=None): def card_vend_request(self, request, pk=None):
auth_token = request.META.get('HTTP_AUTHORIZATION', '')
if secrets.VEND_API_TOKEN and auth_token != 'Bearer ' + secrets.VEND_API_TOKEN:
raise exceptions.PermissionDenied()
source_card = get_object_or_404(models.Card, card_number=pk)
source_user = source_card.user
try: try:
number = request.data['number'] with transaction.atomic():
except KeyError: auth_token = request.META.get('HTTP_AUTHORIZATION', '')
raise exceptions.ValidationError(dict(number='This field is required.')) if secrets.VEND_API_TOKEN and auth_token != 'Bearer ' + secrets.VEND_API_TOKEN:
raise exceptions.PermissionDenied()
try: source_card = get_object_or_404(models.Card, card_number=pk)
balance = float(request.data['balance']) source_user = source_card.user
except KeyError:
raise exceptions.ValidationError(dict(balance='This field is required.'))
except ValueError:
raise exceptions.ValidationError(dict(balance='Invalid number.'))
try: try:
amount = float(request.data['amount']) number = request.data['number']
except KeyError: except KeyError:
raise exceptions.ValidationError(dict(amount='This field is required.')) raise exceptions.ValidationError(dict(number='This field is required.'))
except ValueError:
raise exceptions.ValidationError(dict(amount='Invalid number.'))
if amount < 1.00: try:
raise exceptions.ValidationError(dict(amount='Amount too small.')) balance = float(request.data['balance'])
except KeyError:
raise exceptions.ValidationError(dict(balance='This field is required.'))
except ValueError:
raise exceptions.ValidationError(dict(balance='Invalid number.'))
try:
amount = float(request.data['amount'])
except KeyError:
raise exceptions.ValidationError(dict(amount='This field is required.'))
except ValueError:
raise exceptions.ValidationError(dict(amount='Invalid number.'))
if amount < 1.00:
raise exceptions.ValidationError(dict(amount='Amount too small.'))
source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] source_user_balance = source_user.transactions.aggregate(Sum('protocoin'))['protocoin__sum']
source_user_balance = float(source_user_balance) source_user_balance = float(source_user_balance)
if source_user_balance != balance: if source_user_balance != balance:
raise exceptions.ValidationError(dict(balance='Incorrect current balance.')) raise exceptions.ValidationError(dict(balance='Incorrect current balance.'))
if source_user_balance < amount: if source_user_balance < amount:
raise exceptions.ValidationError(dict(amount='Insufficient funds.')) raise exceptions.ValidationError(dict(amount='Insufficient funds.'))
source_delta = -amount source_delta = -amount
memo = 'Protocoin - Purchase spent ₱ {} on vending machine item #{}'.format( memo = 'Protocoin - Purchase spent ₱ {} on vending machine item #{}'.format(
amount, amount,
number, number,
) )
tx = models.Transaction.objects.create( tx = models.Transaction.objects.create(
user=source_user, user=source_user,
protocoin=source_delta, protocoin=source_delta,
amount=0, amount=0,
number_of_membership_months=0, number_of_membership_months=0,
account_type='Protocoin', account_type='Protocoin',
category='Snacks', category='Snacks',
info_source='System', info_source='System',
memo=memo, memo=memo,
) )
utils.log_transaction(tx) utils.log_transaction(tx)
return Response(200) return Response(200)
except OperationalError:
self.card_vend_request(request, pk)
class RegistrationView(RegisterView): class RegistrationView(RegisterView):