Skip to content

Commit 77946cc

Browse files
authored
Add discounts (#182)
* Add discounts
1 parent aa28ee5 commit 77946cc

File tree

5 files changed

+109
-6
lines changed

5 files changed

+109
-6
lines changed

docs/manuel/src/03-existant/modules/payment.md

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ automatiquement à la création d'un tournoi (et plus tard, d'un créneau pizza)
2020
Ils contiennent plusieurs info comme le nom, le prix, la description, les dates
2121
de début et de fin de vente, etc.
2222

23+
## Discount
24+
25+
Les réductions sont créées manuellement par les admins. Elles font le lien entre
26+
un utilisateur et un produit. La valeur de la réduction et une description sont
27+
également stockées dans ce modèle. Les réductions doivent être ajoutés en amont
28+
du paiement et peuvent être utilisées une seule fois. Cela peut permettre de
29+
réduire le prix d'un pour certains joueurs.
30+
2331
## Transaction
2432

2533
Les transactions sont créées à chaque fois qu'un utilisateur initie un paiement

insalan/payment/admin.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.http import HttpResponse
99
from django.utils.translation import gettext_lazy as _
1010

11-
from .models import Product, Transaction, Payment, TransactionStatus
11+
from .models import Product, Transaction, Payment, TransactionStatus, Discount
1212

1313

1414
class ProductAdmin(admin.ModelAdmin):
@@ -123,3 +123,15 @@ def has_delete_permission(self, _request, _obj=None):
123123

124124

125125
admin.site.register(Transaction, TransactionAdmin)
126+
127+
class DiscountAdmin(admin.ModelAdmin):
128+
"""
129+
Admin handler for Discounts
130+
"""
131+
132+
list_display = ("id", "discount", "user", "product", "used")
133+
search_fields = ["id", "discount", "user", "product", "reason"]
134+
135+
136+
137+
admin.site.register(Discount, DiscountAdmin)

insalan/payment/models.py

+70
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.db import models
1111
from django.utils.translation import gettext_lazy as _
1212
from django.utils import timezone
13+
from django.core.validators import MinValueValidator
1314
from rest_framework.serializers import ValidationError
1415

1516
import insalan.settings as app_settings
@@ -85,6 +86,12 @@ def can_be_bought_now(self) -> bool:
8586
"""Returns whether or not the product can be bought now"""
8687
return self.available_from <= timezone.now() <= self.available_until
8788

89+
def __str__(self):
90+
"""
91+
Return the name of the product
92+
"""
93+
return str(self.name)
94+
8895

8996
class Payment(models.Model):
9097
"""
@@ -161,6 +168,11 @@ class Meta:
161168
decimal_places=2,
162169
verbose_name=_("Montant"),
163170
)
171+
discounts = models.ManyToManyField(
172+
"Discount",
173+
blank=True,
174+
verbose_name=_("Réductions")
175+
)
164176

165177
@staticmethod
166178
def new(**data):
@@ -326,6 +338,10 @@ def validate_transaction(self):
326338

327339
self.payment_status = TransactionStatus.SUCCEEDED
328340
self.last_modification_date = timezone.make_aware(datetime.now())
341+
# For each discount, mark it as used
342+
for discount in self.discounts.all():
343+
discount.use()
344+
329345
self.save()
330346
logger.info("Transaction %s succeeded", self.id)
331347
self.run_success_hooks()
@@ -378,3 +394,57 @@ class Meta:
378394
null=True,
379395
)
380396
count = models.IntegerField(default=1, editable=True, verbose_name=_("Quantité"))
397+
398+
# Discount
399+
400+
class DiscountAlreadyUsedError(Exception):
401+
"""Error raised when trying to use an already used discount"""
402+
403+
class Discount(models.Model):
404+
"""
405+
A discount is a temporary reduction of the price of a product
406+
407+
A discount is tied to a user, a product and can be used only once
408+
"""
409+
410+
class Meta:
411+
"""Meta information"""
412+
413+
verbose_name = _("Réduction")
414+
verbose_name_plural = _("Réductions")
415+
416+
id: int
417+
user = models.ForeignKey(
418+
User, null=True, on_delete=models.SET_NULL, verbose_name=_("Utilisateur")
419+
)
420+
product = models.ForeignKey(
421+
Product, null=True, on_delete=models.SET_NULL, verbose_name=_("Produit")
422+
)
423+
discount = models.DecimalField(
424+
null=False,
425+
max_digits=5,
426+
decimal_places=2,
427+
verbose_name=_("Réduction"),
428+
validators=[MinValueValidator(Decimal('0.01'))]
429+
)
430+
reason = models.CharField(max_length=200, verbose_name=_("Motif"))
431+
creation_date = models.DateTimeField(
432+
verbose_name=_("Date de création"),
433+
editable=False,
434+
default=timezone.now
435+
)
436+
used = models.BooleanField(default=False, verbose_name=_("Utilisé"))
437+
used_date = models.DateTimeField(
438+
verbose_name=_("Date d'utilisation"),
439+
editable=False,
440+
null=True,
441+
blank=True
442+
)
443+
444+
def use(self):
445+
"""Use the discount"""
446+
if self.used:
447+
raise DiscountAlreadyUsedError("Discount already used")
448+
self.used = True
449+
self.used_date = timezone.make_aware(datetime.now())
450+
self.save()

insalan/payment/views.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import insalan.settings as app_settings
2020
import insalan.payment.serializers as serializers
2121

22-
from .models import Transaction, TransactionStatus, Product, Payment
22+
from .models import Transaction, TransactionStatus, Product, Payment, Discount
2323
from .tokens import Token
2424

2525
logger = logging.getLogger(__name__)
@@ -425,9 +425,22 @@ def create(self, request):
425425
status=status.HTTP_400_BAD_REQUEST,
426426
)
427427

428+
amount = transaction_obj.amount
429+
430+
# If the user has a discount for some products, apply them
431+
for product in transaction_obj.products.all():
432+
discounts = Discount.objects.filter(user=payer, product=product, used=False)
433+
for discount in discounts:
434+
# Check if the discount is applicable
435+
if amount >= discount.discount:
436+
amount -= discount.discount
437+
438+
# Add the discount to the transaction object
439+
transaction_obj.discounts.add(discount)
440+
428441
# helloasso intent
429442
helloasso_amount = int(
430-
transaction_obj.amount * 100
443+
amount * 100
431444
) # helloasso reads prices in cents
432445
intent_body = {
433446
"totalAmount": helloasso_amount,

insalan/tournament/models/tournament.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def save(self, *args, **kwargs):
202202
if self.player_online_product is None:
203203
prod = Product.objects.create(
204204
price=self.player_price_online,
205-
name=_(f"Place {self.name} Joueur en ligne"),
205+
name=_(f"Place {self.name} Joueur en ligne - {self.event.name}"),
206206
desc=_(f"Inscription au tournoi {self.name} joueur"),
207207
category=ProductCategory.REGISTRATION_PLAYER,
208208
associated_tournament=self,
@@ -219,7 +219,7 @@ def save(self, *args, **kwargs):
219219
if self.manager_online_product is None:
220220
prod = Product.objects.create(
221221
price=self.manager_price_online,
222-
name=_(f"Place {self.name} manager en ligne"),
222+
name=_(f"Place {self.name} manager en ligne - {self.event.name}"),
223223
desc=_(f"Inscription au tournoi {self.name} manager"),
224224
category=ProductCategory.REGISTRATION_MANAGER,
225225
associated_tournament=self,
@@ -236,7 +236,7 @@ def save(self, *args, **kwargs):
236236
if self.substitute_online_product is None:
237237
prod = Product.objects.create(
238238
price=self.substitute_price_online,
239-
name=_(f"Place {self.name} remplaçant en ligne"),
239+
name=_(f"Place {self.name} remplaçant en ligne - {self.event.name}"),
240240
desc=_(f"Inscription au tournoi {self.name} remplaçant"),
241241
category=ProductCategory.REGISTRATION_SUBSTITUTE,
242242
associated_tournament=self,

0 commit comments

Comments
 (0)