diff --git a/docs/manuel/src/03-existant/modules/payment.md b/docs/manuel/src/03-existant/modules/payment.md index db3b742f..e976322e 100644 --- a/docs/manuel/src/03-existant/modules/payment.md +++ b/docs/manuel/src/03-existant/modules/payment.md @@ -20,6 +20,14 @@ automatiquement à la création d'un tournoi (et plus tard, d'un créneau pizza) Ils contiennent plusieurs info comme le nom, le prix, la description, les dates de début et de fin de vente, etc. +## Discount + +Les réductions sont créées manuellement par les admins. Elles font le lien entre +un utilisateur et un produit. La valeur de la réduction et une description sont +également stockées dans ce modèle. Les réductions doivent être ajoutés en amont +du paiement et peuvent être utilisées une seule fois. Cela peut permettre de +réduire le prix d'un pour certains joueurs. + ## Transaction Les transactions sont créées à chaque fois qu'un utilisateur initie un paiement diff --git a/insalan/payment/admin.py b/insalan/payment/admin.py index 22c89cd5..1ea83abc 100644 --- a/insalan/payment/admin.py +++ b/insalan/payment/admin.py @@ -8,7 +8,7 @@ from django.http import HttpResponse from django.utils.translation import gettext_lazy as _ -from .models import Product, Transaction, Payment, TransactionStatus +from .models import Product, Transaction, Payment, TransactionStatus, Discount class ProductAdmin(admin.ModelAdmin): @@ -123,3 +123,15 @@ def has_delete_permission(self, _request, _obj=None): admin.site.register(Transaction, TransactionAdmin) + +class DiscountAdmin(admin.ModelAdmin): + """ + Admin handler for Discounts + """ + + list_display = ("id", "discount", "user", "product", "used") + search_fields = ["id", "discount", "user", "product", "reason"] + + + +admin.site.register(Discount, DiscountAdmin) diff --git a/insalan/payment/models.py b/insalan/payment/models.py index c1f3c5c1..7fe28e57 100644 --- a/insalan/payment/models.py +++ b/insalan/payment/models.py @@ -10,6 +10,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils import timezone +from django.core.validators import MinValueValidator from rest_framework.serializers import ValidationError import insalan.settings as app_settings @@ -85,6 +86,12 @@ def can_be_bought_now(self) -> bool: """Returns whether or not the product can be bought now""" return self.available_from <= timezone.now() <= self.available_until + def __str__(self): + """ + Return the name of the product + """ + return str(self.name) + class Payment(models.Model): """ @@ -161,6 +168,11 @@ class Meta: decimal_places=2, verbose_name=_("Montant"), ) + discounts = models.ManyToManyField( + "Discount", + blank=True, + verbose_name=_("Réductions") + ) @staticmethod def new(**data): @@ -326,6 +338,10 @@ def validate_transaction(self): self.payment_status = TransactionStatus.SUCCEEDED self.last_modification_date = timezone.make_aware(datetime.now()) + # For each discount, mark it as used + for discount in self.discounts.all(): + discount.use() + self.save() logger.info("Transaction %s succeeded", self.id) self.run_success_hooks() @@ -378,3 +394,57 @@ class Meta: null=True, ) count = models.IntegerField(default=1, editable=True, verbose_name=_("Quantité")) + +# Discount + +class DiscountAlreadyUsedError(Exception): + """Error raised when trying to use an already used discount""" + +class Discount(models.Model): + """ + A discount is a temporary reduction of the price of a product + + A discount is tied to a user, a product and can be used only once + """ + + class Meta: + """Meta information""" + + verbose_name = _("Réduction") + verbose_name_plural = _("Réductions") + + id: int + user = models.ForeignKey( + User, null=True, on_delete=models.SET_NULL, verbose_name=_("Utilisateur") + ) + product = models.ForeignKey( + Product, null=True, on_delete=models.SET_NULL, verbose_name=_("Produit") + ) + discount = models.DecimalField( + null=False, + max_digits=5, + decimal_places=2, + verbose_name=_("Réduction"), + validators=[MinValueValidator(Decimal('0.01'))] + ) + reason = models.CharField(max_length=200, verbose_name=_("Motif")) + creation_date = models.DateTimeField( + verbose_name=_("Date de création"), + editable=False, + default=timezone.now + ) + used = models.BooleanField(default=False, verbose_name=_("Utilisé")) + used_date = models.DateTimeField( + verbose_name=_("Date d'utilisation"), + editable=False, + null=True, + blank=True + ) + + def use(self): + """Use the discount""" + if self.used: + raise DiscountAlreadyUsedError("Discount already used") + self.used = True + self.used_date = timezone.make_aware(datetime.now()) + self.save() diff --git a/insalan/payment/views.py b/insalan/payment/views.py index 7414f837..056e49e6 100644 --- a/insalan/payment/views.py +++ b/insalan/payment/views.py @@ -19,7 +19,7 @@ import insalan.settings as app_settings import insalan.payment.serializers as serializers -from .models import Transaction, TransactionStatus, Product, Payment +from .models import Transaction, TransactionStatus, Product, Payment, Discount from .tokens import Token logger = logging.getLogger(__name__) @@ -425,9 +425,22 @@ def create(self, request): status=status.HTTP_400_BAD_REQUEST, ) + amount = transaction_obj.amount + + # If the user has a discount for some products, apply them + for product in transaction_obj.products.all(): + discounts = Discount.objects.filter(user=payer, product=product, used=False) + for discount in discounts: + # Check if the discount is applicable + if amount >= discount.discount: + amount -= discount.discount + + # Add the discount to the transaction object + transaction_obj.discounts.add(discount) + # helloasso intent helloasso_amount = int( - transaction_obj.amount * 100 + amount * 100 ) # helloasso reads prices in cents intent_body = { "totalAmount": helloasso_amount, diff --git a/insalan/tournament/models/tournament.py b/insalan/tournament/models/tournament.py index 4b8e9d94..5dd6df9b 100644 --- a/insalan/tournament/models/tournament.py +++ b/insalan/tournament/models/tournament.py @@ -195,7 +195,7 @@ def save(self, *args, **kwargs): if self.player_online_product is None: prod = Product.objects.create( price=self.player_price_online, - name=_(f"Place {self.name} Joueur en ligne"), + name=_(f"Place {self.name} Joueur en ligne - {self.event.name}"), desc=_(f"Inscription au tournoi {self.name} joueur"), category=ProductCategory.REGISTRATION_PLAYER, associated_tournament=self, @@ -212,7 +212,7 @@ def save(self, *args, **kwargs): if self.manager_online_product is None: prod = Product.objects.create( price=self.manager_price_online, - name=_(f"Place {self.name} manager en ligne"), + name=_(f"Place {self.name} manager en ligne - {self.event.name}"), desc=_(f"Inscription au tournoi {self.name} manager"), category=ProductCategory.REGISTRATION_MANAGER, associated_tournament=self, @@ -229,7 +229,7 @@ def save(self, *args, **kwargs): if self.substitute_online_product is None: prod = Product.objects.create( price=self.substitute_price_online, - name=_(f"Place {self.name} remplaçant en ligne"), + name=_(f"Place {self.name} remplaçant en ligne - {self.event.name}"), desc=_(f"Inscription au tournoi {self.name} remplaçant"), category=ProductCategory.REGISTRATION_SUBSTITUTE, associated_tournament=self,