Make protocoin transactions atomic to fix race conditions

master
Tanner Collin 2 years ago
parent 226008a0c2
commit 9bb80f6dce
  1. 228
      apiserver/apiserver/api/views.py

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

Loading…
Cancel
Save