🐛 Prevent transactions where note balances go out integer bounds

This commit is contained in:
Yohann D'ANELLO 2020-08-05 16:23:32 +02:00
parent acf7ecc4ae
commit af857d6fae
7 changed files with 164 additions and 92 deletions

View File

@ -5,6 +5,7 @@ from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
@ -209,5 +210,23 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer):
except ImportError: # Activity app is not loaded
pass
def validate(self, attrs):
resource_type = attrs.pop(self.resource_type_field_name)
serializer = self._get_serializer_from_resource_type(resource_type)
if self.instance:
instance = self.instance
info = model_meta.get_field_info(instance)
for attr, value in attrs.items():
if attr in info.relations and info.relations[attr].to_many:
field = getattr(instance, attr)
field.set(value)
else:
setattr(instance, attr, value)
instance.validate(True)
else:
serializer.Meta.model(**attrs).validate(True)
attrs[self.resource_type_field_name] = resource_type
return super().validate(attrs)
class Meta:
model = Transaction

View File

@ -164,10 +164,43 @@ class Transaction(PolymorphicModel):
models.Index(fields=['destination']),
]
def validate(self, reset=False):
previous_source_balance = self.source.balance
previous_dest_balance = self.destination.balance
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:
# Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk)
if old_transaction.valid:
self.source.balance += to_transfer
self.destination.balance -= to_transfer
if self.valid:
self.source.balance -= to_transfer
self.destination.balance += to_transfer
# When a transaction is declared valid, we ensure that the invalidity reason is null, if it was
# previously invalid
self.invalidity_reason = None
source_balance = self.source.balance
dest_balance = self.destination.balance
if reset:
self.source.balance = previous_source_balance
self.destination.balance = previous_dest_balance
if source_balance > 2147483647 or source_balance < -2147483648\
or dest_balance > 2147483647 or dest_balance < -2147483648:
raise ValidationError(_("The note balances must be between - 21 474 836.47 € and 21 474 836.47 €."))
def save(self, *args, **kwargs):
"""
When saving, also transfer money between two notes
"""
self.validate(False)
if not self.source.is_active or not self.destination.is_active:
if 'force_insert' not in kwargs or not kwargs['force_insert']:
@ -187,23 +220,6 @@ class Transaction(PolymorphicModel):
super().save(*args, **kwargs)
return
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:
# Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk)
if old_transaction.valid:
self.source.balance += to_transfer
self.destination.balance -= to_transfer
if self.valid:
self.source.balance -= to_transfer
self.destination.balance += to_transfer
# When a transaction is declared valid, we ensure that the invalidity reason is null, if it was
# previously invalid
self.invalidity_reason = None
# We save first the transaction, in case of the user has no right to transfer money
super().save(*args, **kwargs)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-08-05 13:58+0200\n"
"POT-Creation-Date: 2020-08-05 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -46,7 +46,7 @@ msgstr ""
#: apps/activity/models.py:24 apps/activity/models.py:49
#: apps/member/models.py:162 apps/note/models/notes.py:212
#: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45
#: apps/note/models/transactions.py:268 apps/permission/models.py:339
#: apps/note/models/transactions.py:284 apps/permission/models.py:339
#: apps/wei/models.py:67 apps/wei/models.py:119
#: templates/member/club_info.html:13 templates/member/profile_info.html:14
#: templates/registration/future_profile_detail.html:16
@ -550,7 +550,7 @@ msgstr ""
msgid "The role {role} does not apply to the club {club}."
msgstr ""
#: apps/member/models.py:353 apps/member/views.py:589
#: apps/member/models.py:353 apps/member/views.py:588
msgid "User is already a member of the club"
msgstr ""
@ -585,71 +585,71 @@ msgstr ""
msgid "This address must be valid."
msgstr ""
#: apps/member/views.py:134
#: apps/member/views.py:133
msgid "Profile detail"
msgstr ""
#: apps/member/views.py:168
#: apps/member/views.py:167
msgid "Search user"
msgstr ""
#: apps/member/views.py:202 apps/member/views.py:388
#: apps/member/views.py:201 apps/member/views.py:387
msgid "Note aliases"
msgstr ""
#: apps/member/views.py:216
#: apps/member/views.py:215
msgid "Update note picture"
msgstr ""
#: apps/member/views.py:274 templates/member/profile_info.html:43
#: apps/member/views.py:273 templates/member/profile_info.html:43
msgid "Manage auth token"
msgstr ""
#: apps/member/views.py:302
#: apps/member/views.py:301
msgid "Create new club"
msgstr ""
#: apps/member/views.py:314
#: apps/member/views.py:313
msgid "Search club"
msgstr ""
#: apps/member/views.py:339
#: apps/member/views.py:338
msgid "Club detail"
msgstr ""
#: apps/member/views.py:405
#: apps/member/views.py:404
msgid "Update club"
msgstr ""
#: apps/member/views.py:439
#: apps/member/views.py:438
msgid "Add new member to the club"
msgstr ""
#: apps/member/views.py:580 apps/wei/views.py:862
#: apps/member/views.py:579 apps/wei/views.py:862
msgid ""
"This user don't have enough money to join this club, and can't have a "
"negative balance."
msgstr ""
#: apps/member/views.py:593
#: apps/member/views.py:592
msgid "The membership must start after {:%m-%d-%Y}."
msgstr ""
#: apps/member/views.py:598
#: apps/member/views.py:597
msgid "The membership must begin before {:%m-%d-%Y}."
msgstr ""
#: apps/member/views.py:615 apps/member/views.py:617 apps/member/views.py:619
#: apps/member/views.py:614 apps/member/views.py:616 apps/member/views.py:618
#: apps/registration/views.py:290 apps/registration/views.py:292
#: apps/registration/views.py:294 apps/wei/views.py:867 apps/wei/views.py:871
msgid "This field is required."
msgstr ""
#: apps/member/views.py:703
#: apps/member/views.py:702
msgid "Manage roles of an user in the club"
msgstr ""
#: apps/member/views.py:728
#: apps/member/views.py:727
msgid "Members of the club"
msgstr ""
@ -866,68 +866,73 @@ msgstr ""
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:175
#: apps/note/models/transactions.py:197
msgid ""
"The note balances must be between - 21 474 836.47 € and 21 474 836.47 €."
msgstr ""
#: apps/note/models/transactions.py:208
msgid ""
"The transaction can't be saved since the source note or the destination note "
"is not active."
msgstr ""
#: apps/note/models/transactions.py:230
#: apps/note/models/transactions.py:246
#: templates/activity/activity_entry.html:13 templates/base.html:99
#: templates/note/transaction_form.html:15
#: templates/note/transaction_form.html:143
msgid "Transfer"
msgstr ""
#: apps/note/models/transactions.py:254
#: apps/note/models/transactions.py:270
msgid "Template"
msgstr ""
#: apps/note/models/transactions.py:257
#: apps/note/models/transactions.py:273
msgid "recurrent transaction"
msgstr ""
#: apps/note/models/transactions.py:258
#: apps/note/models/transactions.py:274
msgid "recurrent transactions"
msgstr ""
#: apps/note/models/transactions.py:273
#: apps/note/models/transactions.py:289
msgid "first_name"
msgstr ""
#: apps/note/models/transactions.py:278
#: apps/note/models/transactions.py:294
msgid "bank"
msgstr ""
#: apps/note/models/transactions.py:284
#: apps/note/models/transactions.py:300
#: templates/activity/activity_entry.html:17
#: templates/note/transaction_form.html:20
msgid "Credit"
msgstr ""
#: apps/note/models/transactions.py:284 templates/note/transaction_form.html:24
#: apps/note/models/transactions.py:300 templates/note/transaction_form.html:24
msgid "Debit"
msgstr ""
#: apps/note/models/transactions.py:295
#: apps/note/models/transactions.py:311
msgid ""
"A special transaction is only possible between a Note associated to a "
"payment method and a User or a Club"
msgstr ""
#: apps/note/models/transactions.py:299
#: apps/note/models/transactions.py:315
msgid "Special transaction"
msgstr ""
#: apps/note/models/transactions.py:300
#: apps/note/models/transactions.py:316
msgid "Special transactions"
msgstr ""
#: apps/note/models/transactions.py:316 apps/note/models/transactions.py:321
#: apps/note/models/transactions.py:332 apps/note/models/transactions.py:337
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:317 apps/treasury/models.py:228
#: apps/note/models/transactions.py:333 apps/treasury/models.py:228
msgid "membership transactions"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-08-05 13:58+0200\n"
"POT-Creation-Date: 2020-08-05 16:18+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -47,7 +47,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
#: apps/activity/models.py:24 apps/activity/models.py:49
#: apps/member/models.py:162 apps/note/models/notes.py:212
#: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45
#: apps/note/models/transactions.py:268 apps/permission/models.py:339
#: apps/note/models/transactions.py:284 apps/permission/models.py:339
#: apps/wei/models.py:67 apps/wei/models.py:119
#: templates/member/club_info.html:13 templates/member/profile_info.html:14
#: templates/registration/future_profile_detail.html:16
@ -555,7 +555,7 @@ msgstr "l'adhésion finit le"
msgid "The role {role} does not apply to the club {club}."
msgstr "Le rôle {role} ne s'applique pas au club {club}."
#: apps/member/models.py:353 apps/member/views.py:589
#: apps/member/models.py:353 apps/member/views.py:588
msgid "User is already a member of the club"
msgstr "L'utilisateur est déjà membre du club"
@ -590,47 +590,47 @@ msgstr "Modifier le profil"
msgid "This address must be valid."
msgstr "Cette adresse doit être valide."
#: apps/member/views.py:134
#: apps/member/views.py:133
msgid "Profile detail"
msgstr "Détails de l'utilisateur"
#: apps/member/views.py:168
#: apps/member/views.py:167
msgid "Search user"
msgstr "Chercher un utilisateur"
#: apps/member/views.py:202 apps/member/views.py:388
#: apps/member/views.py:201 apps/member/views.py:387
msgid "Note aliases"
msgstr "Alias de la note"
#: apps/member/views.py:216
#: apps/member/views.py:215
msgid "Update note picture"
msgstr "Modifier la photo de la note"
#: apps/member/views.py:274 templates/member/profile_info.html:43
#: apps/member/views.py:273 templates/member/profile_info.html:43
msgid "Manage auth token"
msgstr "Gérer les jetons d'authentification"
#: apps/member/views.py:302
#: apps/member/views.py:301
msgid "Create new club"
msgstr "Créer un nouveau club"
#: apps/member/views.py:314
#: apps/member/views.py:313
msgid "Search club"
msgstr "Chercher un club"
#: apps/member/views.py:339
#: apps/member/views.py:338
msgid "Club detail"
msgstr "Détails du club"
#: apps/member/views.py:405
#: apps/member/views.py:404
msgid "Update club"
msgstr "Modifier le club"
#: apps/member/views.py:439
#: apps/member/views.py:438
msgid "Add new member to the club"
msgstr "Ajouter un nouveau membre au club"
#: apps/member/views.py:580 apps/wei/views.py:862
#: apps/member/views.py:579 apps/wei/views.py:862
msgid ""
"This user don't have enough money to join this club, and can't have a "
"negative balance."
@ -638,25 +638,25 @@ msgstr ""
"Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas "
"avoir un solde négatif."
#: apps/member/views.py:593
#: apps/member/views.py:592
msgid "The membership must start after {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}."
#: apps/member/views.py:598
#: apps/member/views.py:597
msgid "The membership must begin before {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}."
#: apps/member/views.py:615 apps/member/views.py:617 apps/member/views.py:619
#: apps/member/views.py:614 apps/member/views.py:616 apps/member/views.py:618
#: apps/registration/views.py:290 apps/registration/views.py:292
#: apps/registration/views.py:294 apps/wei/views.py:867 apps/wei/views.py:871
msgid "This field is required."
msgstr "Ce champ est requis."
#: apps/member/views.py:703
#: apps/member/views.py:702
msgid "Manage roles of an user in the club"
msgstr "Gérer les rôles d'un utilisateur dans le club"
#: apps/member/views.py:728
#: apps/member/views.py:727
msgid "Members of the club"
msgstr "Membres du club"
@ -874,7 +874,14 @@ msgstr "Transaction"
msgid "transactions"
msgstr "Transactions"
#: apps/note/models/transactions.py:175
#: apps/note/models/transactions.py:197
msgid ""
"The note balances must be between - 21 474 836.47 € and 21 474 836.47 €."
msgstr ""
"Les montants des notes doivent se trouver entre - 21 474 836.47 € et 21 474 "
"836.47 €. Ne cherchez pas à capitaliser l'argent du BDE."
#: apps/note/models/transactions.py:208
msgid ""
"The transaction can't be saved since the source note or the destination note "
"is not active."
@ -882,44 +889,44 @@ msgstr ""
"La transaction ne peut pas être sauvegardée puisque la note source ou la "
"note de destination n'est pas active."
#: apps/note/models/transactions.py:230
#: apps/note/models/transactions.py:246
#: templates/activity/activity_entry.html:13 templates/base.html:99
#: templates/note/transaction_form.html:15
#: templates/note/transaction_form.html:143
msgid "Transfer"
msgstr "Virement"
#: apps/note/models/transactions.py:254
#: apps/note/models/transactions.py:270
msgid "Template"
msgstr "Bouton"
#: apps/note/models/transactions.py:257
#: apps/note/models/transactions.py:273
msgid "recurrent transaction"
msgstr "Transaction issue de bouton"
#: apps/note/models/transactions.py:258
#: apps/note/models/transactions.py:274
msgid "recurrent transactions"
msgstr "Transactions issues de boutons"
#: apps/note/models/transactions.py:273
#: apps/note/models/transactions.py:289
msgid "first_name"
msgstr "prénom"
#: apps/note/models/transactions.py:278
#: apps/note/models/transactions.py:294
msgid "bank"
msgstr "banque"
#: apps/note/models/transactions.py:284
#: apps/note/models/transactions.py:300
#: templates/activity/activity_entry.html:17
#: templates/note/transaction_form.html:20
msgid "Credit"
msgstr "Crédit"
#: apps/note/models/transactions.py:284 templates/note/transaction_form.html:24
#: apps/note/models/transactions.py:300 templates/note/transaction_form.html:24
msgid "Debit"
msgstr "Débit"
#: apps/note/models/transactions.py:295
#: apps/note/models/transactions.py:311
msgid ""
"A special transaction is only possible between a Note associated to a "
"payment method and a User or a Club"
@ -927,19 +934,19 @@ msgstr ""
"Une transaction spéciale n'est possible que entre une note associée à un "
"mode de paiement et un utilisateur ou un club."
#: apps/note/models/transactions.py:299
#: apps/note/models/transactions.py:315
msgid "Special transaction"
msgstr "Transaction de crédit/retrait"
#: apps/note/models/transactions.py:300
#: apps/note/models/transactions.py:316
msgid "Special transactions"
msgstr "Transactions de crédit/retrait"
#: apps/note/models/transactions.py:316 apps/note/models/transactions.py:321
#: apps/note/models/transactions.py:332 apps/note/models/transactions.py:337
msgid "membership transaction"
msgstr "Transaction d'adhésion"
#: apps/note/models/transactions.py:317 apps/treasury/models.py:228
#: apps/note/models/transactions.py:333 apps/treasury/models.py:228
msgid "membership transactions"
msgstr "Transactions d'adhésion"

View File

@ -371,8 +371,12 @@ function de_validate(id, validated) {
refreshHistory();
},
error: function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"];
if (!error)
error = err.responseText;
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + JSON.parse(err.responseText)["detail"], "danger", 10000);
"de cette transaction : " + error, "danger");
refreshBalance();
// error if this method doesn't exist. Please define it.

View File

@ -212,11 +212,11 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
if (newBalance <= -5000)
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " +
"succès, mais la note émettrice " + source_alias + " est en négatif sévère.",
"danger", 10000);
"danger", 30000);
else if (newBalance < 0)
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " +
"succès, mais la note émettrice " + source_alias + " est en négatif.",
"warning", 10000);
"warning", 30000);
}
reset();
}).fail(function (e) {
@ -240,7 +240,7 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000);
}).fail(function () {
reset();
errMsg(e.responseJSON, 10000);
errMsg(e.responseJSON);
});
});
}

View File

@ -213,6 +213,13 @@ $("#btn_transfer").click(function() {
error = true;
}
let amount = Math.floor(100 * amount_field.val());
if (amount > 2147483647) {
amount_field.addClass('is-invalid');
$("#amount-required").html("<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>");
error = true;
}
if (!reason_field.val()) {
reason_field.addClass('is-invalid');
$("#reason-required").html("<strong>Ce champ est requis.</strong>");
@ -232,7 +239,6 @@ $("#btn_transfer").click(function() {
if (error)
return;
let amount = 100 * amount_field.val();
let reason = reason_field.val();
if ($("#type_transfer").is(':checked')) {
@ -277,7 +283,15 @@ $("#btn_transfer").click(function() {
+ " vers la note " + dest.name + " a été fait avec succès !", "success", 10000);
reset();
}).fail(function () { // do it again but valid = false
}).fail(function (err) { // do it again but valid = false
let errObj = JSON.parse(err.responseText);
if (errObj["non_field_errors"]) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + errObj["non_field_errors"], "danger");
return;
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
@ -298,9 +312,13 @@ $("#btn_transfer").click(function() {
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000);
reset();
}).fail(function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]
if (!error)
error = err.responseText;
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
+ " vers la note " + dest.name + " a échoué : " + error, "danger");
});
});
});
@ -346,8 +364,11 @@ $("#btn_transfer").click(function() {
addMsg("Le crédit/retrait a bien été effectué !", "success", 10000);
reset();
}).fail(function (err) {
addMsg("Le crédit/retrait a échoué : " + JSON.parse(err.responseText)["detail"],
"danger", 10000);
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]
if (!error)
error = err.responseText;
addMsg("Le crédit/retrait a échoué : " + error, "danger", 10000);
});
}
});