Make protocoin transactions atomic to fix race conditions
This commit is contained in:
parent
226008a0c2
commit
9bb80f6dce
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user