2
\$\begingroup\$

In an app which it is possible to transfer money between accounts I have the following logic:

View:

class GiftCardRegisterBuyConfirmationView(LoginRequiredMixin, generic.CreateView):
    template_name = "giftcard/buy-card-modal-content.html"
    model = BuyGiftCardOrder
    form_class = BuyGiftCardOrderForm
    success_url = reverse_lazy("giftcard:register-buy-list")

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)

Form:

class BuyGiftCardOrderForm(forms.ModelForm):
    class Meta:
        model = BuyGiftCardOrder
        exclude = ('created_by', 'order_total_btc_value')

    def clean(self):
        cleaned_data = super().clean()
        amount = self.cleaned_data.get("amount")
        if amount:
            with transaction.atomic():
                try:
                    admin_wallet = Wallet.objects.filter(user__is_superuser=True).first()
                    user_wallet = Wallet.objects.select_for_update().get(user=self.user)

                    if user_wallet.balance - amount < 0:
                        raise Exception("Not enough balance")

                    if admin_wallet.pk == user_wallet.pk:
                        raise Exception("Admins can't buy cards. Create a normal account in order to buy one.")

                    user_wallet.transfer(
                        self.total_btc_price,
                        admin_wallet
                    )
                except Exception as e:
                    self.add_error(None, ValidationError(f"Error: {e}"))
        return cleaned_data

Model:

class Wallet(models.Model):
    currency = models.ForeignKey('Currency', on_delete=models.CASCADE)
    balance = models.DecimalField(_('Balance'), max_digits=18, decimal_places=8, default=0)
    holded = models.DecimalField(_('Holded'), max_digits=18, decimal_places=8, default=0)
    unconfirmed = models.DecimalField(_('Unconfirmed'), max_digits=18, decimal_places=8, default=0)
    label = models.CharField(_('Label'), max_length=100, blank=True, null=True, unique=True)

    def __str__(self):
        return u'{0} {1} "{2}"'.format(self.balance, self.currency.ticker, self.label or '')

    def transfer(self, amount, deposite_wallet, reason=None, description=""):
        if amount < 0:
            raise ValueError('Invalid amount')

        if self.balance - amount < 0:
            raise ValueError('No money')

        self.balance -= amount
        self.save()
        deposite_wallet.balance += amount
        deposite_wallet.save()

This works correctly but the problem is that the transaction is happening at the form.

The correct way would be to handle the transfer operation in the view, but in the view, it should happen at the form_valid method. But in in the transfer operation, it would be possible that a user does not have enough money and from what I have researched, errors should not be handled after form is valid..

I could check for balance in form and perform the transfer operation in view. The problem I see is that they should happen within a transaction as it uses select_for_update and I see no way to keep object locked in form and view.

What is the best approach ?

\$\endgroup\$
2
  • 3
    \$\begingroup\$ Hey could you add the model to the question too? For example, the best solution may move some of your code to it. \$\endgroup\$
    – Peilonrayz
    Sep 3, 2020 at 13:34
  • \$\begingroup\$ @Peilonrayz Just edited and added the model. It is already being validated in the model as well.. But how should this model side error be returned to the form in this case? \$\endgroup\$
    – 37dev
    Sep 3, 2020 at 14:02

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.