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 ?