diff --git a/core/tests/test_user.py b/core/tests/test_user.py index 752405b2..0a8a3b7b 100644 --- a/core/tests/test_user.py +++ b/core/tests/test_user.py @@ -200,7 +200,11 @@ class TestFilterInactive(TestCase): ] sale_recipe.make(customer=cls.users[3].customer, date=time_active) baker.make( - Refilling, customer=cls.users[4].customer, date=time_active, counter=counter + Refilling, + customer=cls.users[4].customer, + date=time_active, + counter=counter, + amount=1, ) sale_recipe.make(customer=cls.users[5].customer, date=time_inactive) @@ -455,7 +459,9 @@ def test_user_preferences(client: Client): @pytest.mark.django_db def test_user_stats(client: Client): user = subscriber_user.make() - baker.make(Refilling, customer=user.customer, amount=99999) + baker.make( + Refilling, customer=user.customer, amount=settings.SITH_ACCOUNT_MAX_MONEY + ) bars = [b[0] for b in settings.SITH_COUNTER_BARS] baker.make( Permanency, diff --git a/counter/fields.py b/counter/fields.py index a212059d..caf3a584 100644 --- a/counter/fields.py +++ b/counter/fields.py @@ -1,22 +1,68 @@ from decimal import Decimal from django.conf import settings +from django.core import checks +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.utils.functional import cached_property class CurrencyField(models.DecimalField): """Custom database field used for currency.""" - def __init__(self, *args, **kwargs): - kwargs["max_digits"] = 12 - kwargs["decimal_places"] = 2 - super().__init__(*args, **kwargs) + def __init__( + self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs + ): + kwargs.update({"max_digits": 12, "decimal_places": 2}) + self.min_value = min_value + self.max_value = max_value + super().__init__(verbose_name, name, **kwargs) def to_python(self, value): if value is None: return None return super().to_python(value).quantize(Decimal("0.01")) + @cached_property + def validators(self): + res = [] + if self.max_value: + res.append(MaxValueValidator(self.max_value)) + if self.min_value: + res.append(MinValueValidator(self.min_value)) + return [*super().validators, *res] + + def check(self, **kwargs): # pragma: no cover + # this is executed during runserver, but won't run in prod + errors = super().check(**kwargs) + for name, val in ("min_value", self.min_value), ("max_value", self.max_value): + if not val: + continue + try: + float(val) + except ValueError: + errors.append( + checks.Error( + f"CurrencyField.{name} must be a valid float", + obj=self, + id="sith.E001", + ) + ) + return errors + + def formfield(self, **kwargs): + return super().formfield( + **{"min_value": self.min_value, "max_value": self.max_value, **kwargs} + ) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if self.min_value is not None: + kwargs["min_value"] = self.min_value + if self.max_value is not None: + kwargs["max_value"] = self.max_value + return name, path, args, kwargs + if settings.TESTING: from model_bakery import baker diff --git a/counter/forms.py b/counter/forms.py index 6641a80e..cd99c95a 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -3,13 +3,16 @@ import math import uuid from collections import defaultdict from datetime import date, datetime, timezone +from typing import ClassVar from dateutil.relativedelta import relativedelta from django import forms +from django.conf import settings from django.core.exceptions import ValidationError from django.db.models import Exists, OuterRef, Q from django.forms import BaseModelFormSet from django.http import HttpRequest +from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import ClockedSchedule @@ -168,18 +171,19 @@ class RefillForm(forms.ModelForm): error_css_class = "error" required_css_class = "required" - amount = forms.FloatField( - min_value=0, widget=forms.NumberInput(attrs={"class": "focus"}) - ) class Meta: model = Refilling fields = ["amount", "payment_method"] widgets = {"payment_method": forms.RadioSelect} - def __init__(self, *args, **kwargs): + def __init__( + self, *args, counter: Counter, operator: User, customer: Customer, **kwargs + ): super().__init__(*args, **kwargs) - + max_value = settings.SITH_ACCOUNT_MAX_MONEY - customer.amount + # server-side max_value validation is done by Refilling.clean + self.fields["amount"].widget.attrs["max"] = max_value self.fields["payment_method"].choices = ( method for method in self.fields["payment_method"].choices @@ -187,6 +191,9 @@ class RefillForm(forms.ModelForm): ) if self.fields["payment_method"].initial not in self.allowed_refilling_methods: self.fields["payment_method"].initial = self.allowed_refilling_methods[0] + self.instance.counter = counter + self.instance.operator = operator + self.instance.customer = customer class CounterEditForm(forms.ModelForm): @@ -560,16 +567,7 @@ class BasketItemForm(forms.Form): quantity = forms.IntegerField(min_value=1, required=True) price_id = forms.IntegerField(min_value=0, required=True) - def __init__( - self, - customer: Customer, - counter: Counter, - allowed_prices: dict[int, Price], - *args, - **kwargs, - ): - self.customer = customer # Used by formset - self.counter = counter # Used by formset + def __init__(self, allowed_prices: dict[int, Price], *args, **kwargs): self.allowed_prices = allowed_prices super().__init__(*args, **kwargs) @@ -604,6 +602,15 @@ class BasketItemForm(forms.Form): class BaseBasketForm(forms.BaseFormSet): + # Minimum amount of money there must be on the account after the transaction + # If None, the min balance check is skipped + min_result_balance: ClassVar[int | None] = 0 + + def __init__(self, *args, customer: Customer, counter: Counter, **kwargs): + super().__init__(*args, **kwargs) + self.customer = customer + self.counter = counter + def clean(self): self.forms = [form for form in self.forms if form.cleaned_data != {}] @@ -612,8 +619,8 @@ class BaseBasketForm(forms.BaseFormSet): self._check_forms_have_errors() self._check_product_are_unique() - self._check_recorded_products(self[0].customer) - self._check_enough_money(self[0].counter, self[0].customer) + self._check_recorded_products() + self._check_account_balance() def _check_forms_have_errors(self): if any(len(form.errors) > 0 for form in self): @@ -624,12 +631,35 @@ class BaseBasketForm(forms.BaseFormSet): if len(price_ids) != len(self.forms): raise forms.ValidationError(_("Duplicated product entries.")) - def _check_enough_money(self, counter: Counter, customer: Customer): - self.total_price = sum([data["total_price"] for data in self.cleaned_data]) - if self.total_price > customer.amount: - raise forms.ValidationError(_("Not enough money")) + @cached_property + def total_price(self): + refill = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING + total_other = sum( + form.cleaned_data["total_price"] + for form in self.forms + if form.price.product.product_type_id != refill + ) + total_refill = sum( + form.cleaned_data["total_price"] + for form in self.forms + if form.price.product.product_type_id == refill + ) + return total_other - total_refill - def _check_recorded_products(self, customer: Customer): + def _check_account_balance(self): + result_balance = self.customer.amount - self.total_price + if ( + self.min_result_balance is not None + and self.min_result_balance > result_balance + ): + raise forms.ValidationError(_("Not enough money")) + if result_balance > settings.SITH_ACCOUNT_MAX_MONEY: + raise ValidationError( + _("There cannot be more than %(money)d€ on an AE account") + % {"money": settings.SITH_ACCOUNT_MAX_MONEY} + ) + + def _check_recorded_products(self): """Check for, among other things, ecocups and pitchers""" items = defaultdict(int) for form in self.forms: @@ -638,7 +668,7 @@ class BaseBasketForm(forms.BaseFormSet): returnables = list( ReturnableProduct.objects.filter( Q(product_id__in=ids) | Q(returned_product_id__in=ids) - ).annotate_balance_for(customer) + ).annotate_balance_for(self.customer) ) limit_reached = [] for returnable in returnables: diff --git a/counter/migrations/0042_alter_customer_amount_alter_refilling_amount.py b/counter/migrations/0042_alter_customer_amount_alter_refilling_amount.py new file mode 100644 index 00000000..7c0c6625 --- /dev/null +++ b/counter/migrations/0042_alter_customer_amount_alter_refilling_amount.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.15 on 2026-06-07 12:08 + +from django.db import migrations + +import counter.fields + + +class Migration(migrations.Migration): + dependencies = [("counter", "0041_alter_billinginfo_country_and_more")] + + operations = [ + migrations.AlterField( + model_name="customer", + name="amount", + field=counter.fields.CurrencyField( + decimal_places=2, + default=0, + max_digits=12, + max_value=250, + verbose_name="amount", + ), + ), + migrations.AlterField( + model_name="refilling", + name="amount", + field=counter.fields.CurrencyField( + decimal_places=2, max_digits=12, min_value=0.01, verbose_name="amount" + ), + ), + ] diff --git a/counter/models.py b/counter/models.py index 1907d2fb..cda5ac1b 100644 --- a/counter/models.py +++ b/counter/models.py @@ -28,7 +28,7 @@ from dict2xml import dict2xml from django.conf import settings from django.core.validators import MinLengthValidator from django.db import models -from django.db.models import Exists, F, OuterRef, Q, QuerySet, Subquery, Sum, Value +from django.db.models import Exists, F, Max, OuterRef, Q, QuerySet, Subquery, Sum, Value from django.db.models.functions import Coalesce, Concat, Length from django.forms import ValidationError from django.urls import reverse @@ -99,7 +99,9 @@ class Customer(models.Model): user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE) account_id = models.CharField(_("account id"), max_length=10, unique=True) - amount = CurrencyField(_("amount"), default=0) + amount: CurrencyField = CurrencyField( + _("amount"), max_value=settings.SITH_ACCOUNT_MAX_MONEY, default=0 + ) objects = CustomerQuerySet.as_manager() @@ -156,13 +158,15 @@ class Customer(models.Model): unique_fields=["customer", "returnable"], ) - @property + @cached_property def can_buy(self) -> bool: """Check if whether this customer has the right to purchase any item.""" - subscription = self.user.subscriptions.order_by("subscription_end").last() - if subscription is None: + subscription_end = self.user.subscriptions.aggregate( + res=Max("subscription_end") + ).get("res") + if subscription_end is None: return False - return (date.today() - subscription.subscription_end) < timedelta(days=90) + return (date.today() - subscription_end) < timedelta(days=90) @classmethod def get_or_create(cls, user: User) -> tuple[Customer, bool]: @@ -823,7 +827,7 @@ class Refilling(models.Model): counter = models.ForeignKey( Counter, related_name="refillings", blank=False, on_delete=models.CASCADE ) - amount = CurrencyField(_("amount")) + amount: CurrencyField = CurrencyField(_("amount"), min_value=0.01) operator = models.ForeignKey( User, related_name="refillings_as_operator", @@ -877,6 +881,14 @@ class Refilling(models.Model): return False return user.is_owner(self.counter) and self.payment_method != "CARD" + def clean(self): + super().clean() + if (self.amount + self.customer.amount) > settings.SITH_ACCOUNT_MAX_MONEY: + raise ValidationError( + _("There cannot be more than %(money)d€ on an AE account") + % {"money": settings.SITH_ACCOUNT_MAX_MONEY} + ) + def delete(self, *args, **kwargs): self.customer.amount -= self.amount self.customer.save() diff --git a/counter/static/bundled/counter/components/counter-product-select-index.ts b/counter/static/bundled/counter/components/counter-product-select-index.ts index d4e96a81..433ae9f9 100644 --- a/counter/static/bundled/counter/components/counter-product-select-index.ts +++ b/counter/static/bundled/counter/components/counter-product-select-index.ts @@ -6,7 +6,7 @@ const productParsingRegex = /^(\d+x)?(.*)/i; const codeParsingRegex = / \((\w+)\)$/; function parseProduct(query: string): [number, string] { - const parsed = productParsingRegex.exec(query); + const parsed = productParsingRegex.exec(query) as RegExpExecArray; return [Number.parseInt(parsed[1] || "1", 10), parsed[2]]; } diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index 7228f067..5504cd12 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -3,7 +3,6 @@ import { BasketItem } from "#counter:counter/basket"; import type { CounterConfig, CounterItem, - ErrorMessage, ProductFormula, } from "#counter:counter/types"; import type { CounterProductSelect } from "./components/counter-product-select-index"; @@ -24,7 +23,7 @@ document.addEventListener("alpine:init", () => { } } - this.codeField = this.$refs.codeField; + this.codeField = this.$refs.codeField as CounterProductSelect; this.codeField.widget.hook("after", "onOptionSelect", () => { this.handleCode(); }); @@ -34,14 +33,14 @@ document.addEventListener("alpine:init", () => { // of a formset so we dynamically apply it here this.$refs.basketManagementForm .querySelector("#id_form-TOTAL_FORMS") - .setAttribute(":value", "getBasketSize()"); + ?.setAttribute(":value", "getBasketSize()"); }, removeFromBasket(id: string) { delete this.basket[id]; }, - addToBasket(id: string, quantity: number): ErrorMessage { + addToBasket(id: string, quantity: number) { const item: BasketItem = this.basket[id] || new BasketItem(config.products[id], 0); @@ -50,7 +49,7 @@ document.addEventListener("alpine:init", () => { if (item.quantity <= 0) { delete this.basket[id]; - return ""; + return; } this.basket[id] = item; @@ -72,7 +71,7 @@ document.addEventListener("alpine:init", () => { const products = new Set( Object.values(this.basket).map((item: BasketItem) => item.product.productId), ); - const formula: ProductFormula = config.formulas.find((f: ProductFormula) => { + const formula = config.formulas.find((f: ProductFormula) => { return f.products.every((p: number) => products.has(p)); }); if (formula === undefined) { @@ -80,9 +79,13 @@ document.addEventListener("alpine:init", () => { } // Now that the formula is found, remove the items composing it from the basket for (const product of formula.products) { - const key = Object.entries(this.basket).find( + const item = Object.entries(this.basket).find( ([_, i]: [string, BasketItem]) => i.product.productId === product, - )[0]; + ); + if (item === undefined) { + continue; + } + const key = item[0]; this.basket[key].quantity -= 1; if (this.basket[key].quantity <= 0) { this.removeFromBasket(key); @@ -92,7 +95,7 @@ document.addEventListener("alpine:init", () => { const result = Object.values(config.products) .filter((item: CounterItem) => item.productId === formula.result) .reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr)); - this.addToBasket(result.price.id, 1); + this.addToBasket(result.price.id.toString(), 1); this.alertMessage.display( interpolate( gettext("Formula %(formula)s applied"), @@ -119,14 +122,18 @@ document.addEventListener("alpine:init", () => { }, onRefillingSuccess(event: CustomEvent) { - if (event.type !== "htmx:after-request" || event.detail.failed) { + if ( + event.type !== "htmx:after-swap" || + event.detail.failed || + event.detail.elt.querySelector(".errorlist") + ) { return; } this.customerBalance += Number.parseFloat( (event.detail.target.querySelector("#id_amount") as HTMLInputElement).value, ); - document.getElementById("selling-accordion").setAttribute("open", ""); - this.codeField.widget.focus(); + document.getElementById("selling-accordion")?.setAttribute("open", ""); + this.codeField?.widget.focus(); }, finish() { @@ -136,7 +143,7 @@ document.addEventListener("alpine:init", () => { }); return; } - this.$refs.basketForm.submit(); + (this.$refs.basketForm as HTMLFormElement).submit(); }, cancel() { @@ -144,6 +151,8 @@ document.addEventListener("alpine:init", () => { }, handleCode() { + if (!this.codeField) throw Error("Unexpected null codeField."); + const [quantity, code] = this.codeField.getSelectedProduct() as [number, string]; if (this.codeField.getOperationCodes().includes(code.toUpperCase())) { diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index e93b1110..d18f025e 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -176,13 +176,17 @@ -
+
{% trans %}Refilling{% endtrans %} {% if object.type == "BAR" %} {% if refilling_fragment %}
{{ refilling_fragment }}
diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index aa771b25..69f3bff6 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -144,6 +144,8 @@ class TestRefilling(TestFullClickBase): assert self.updated_amount(self.customer) == 0 def test_refilling_no_refer_fail(self): + """Check that the refill fails is the HTTP_REFERER header is missing""" + def refill(): return self.client.post( reverse( @@ -157,13 +159,13 @@ class TestRefilling(TestFullClickBase): ) self.client.force_login(self.club_admin) - assert refill() + assert refill().status_code == 403 self.client.force_login(self.root) - assert refill() + assert refill().status_code == 403 self.client.force_login(self.subscriber) - assert refill() + assert refill().status_code == 403 assert self.updated_amount(self.customer) == 0 @@ -199,6 +201,17 @@ class TestRefilling(TestFullClickBase): == 404 ) + def test_refilling_above_limit_fails(self): + """Test that it's forbidden to refill a customer above the limit.""" + self.login_in_bar() + limit = settings.SITH_ACCOUNT_MAX_MONEY + # create a refilling to check that current balance is taken into account + baker.make(Refilling, customer=self.customer.customer, amount=limit // 2) + response = self.refill_user(self.customer, self.counter, (limit // 2) + 1) + assert response.status_code == 200 # no redirect = failure + self.customer.customer.refresh_from_db() + assert self.updated_amount(self.customer) == limit // 2 + def test_refilling_counter_success(self): self.login_in_bar() @@ -522,6 +535,19 @@ class TestCounterClick(TestFullClickBase): assert self.updated_amount(self.customer) == Decimal(10) + def test_unrecord_above_limit_fails(self): + """Test that it's forbidden to give back a recorded product + if it puts the account balance above the limit. + """ + self.login_in_bar() + limit = settings.SITH_ACCOUNT_MAX_MONEY + # put the account balance just at the limit + baker.make(Refilling, customer=self.customer.customer, amount=limit) + response = self.submit_basket(self.customer, [BasketItem(self.dcons.id, 1)]) + assert response.status_code == 200 # no redirect = failure + self.customer.customer.refresh_from_db() + assert self.updated_amount(self.customer) == limit + def test_annotate_has_barman_queryset(self): """Test if the custom queryset method `annotate_has_barman` works as intended.""" counters = Counter.objects.annotate_has_barman(self.barmen) diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index 5a194824..ca1b2b85 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -15,6 +15,7 @@ from core.models import User from counter.baker_recipes import product_recipe, refill_recipe, sale_recipe from counter.models import ( Counter, + CounterSellers, Customer, Refilling, ReturnableProduct, @@ -38,7 +39,7 @@ class TestStudentCard(TestCase): cls.subscriber = subscriber_user.make() cls.counter = baker.make(Counter, type="BAR") - cls.counter.sellers.add(cls.barmen) + CounterSellers.objects.create(counter=cls.counter, user=cls.barmen) cls.club_counter = baker.make(Counter) role = baker.make(ClubRole, club=cls.club_counter.club, is_board=True) diff --git a/counter/views/click.py b/counter/views/click.py index 949031c8..9ec678ae 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -24,7 +24,7 @@ from django.shortcuts import get_object_or_404, redirect, resolve_url from django.urls import reverse from django.utils.safestring import SafeString from django.utils.translation import gettext as _ -from django.views.generic import FormView +from django.views.generic import CreateView, FormView from django.views.generic.detail import SingleObjectMixin from ninja.main import HttpRequest @@ -32,7 +32,14 @@ from core.auth.mixins import CanViewMixin from core.models import User from core.views.mixins import FragmentMixin, UseFragmentsMixin from counter.forms import BasketForm, RefillForm -from counter.models import Counter, Customer, ProductFormula, ReturnableProduct, Selling +from counter.models import ( + Counter, + Customer, + ProductFormula, + Refilling, + ReturnableProduct, + Selling, +) from counter.utils import is_logged_in_counter from counter.views.mixins import CounterTabsMixin from counter.views.student_card import StudentCardFormFragment @@ -66,13 +73,13 @@ class CounterClick( current_tab = "counter" def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["form_kwargs"] = { + return super().get_form_kwargs() | { "customer": self.customer, "counter": self.object, - "allowed_prices": {price.id: price for price in self.prices}, + "form_kwargs": { + "allowed_prices": {price.id: price for price in self.prices} + }, } - return kwargs def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"]) @@ -219,9 +226,10 @@ class CounterClick( return kwargs -class RefillingCreateView(FragmentMixin, FormView): +class RefillingCreateView(FragmentMixin, CreateView): """This is a fragment only view which integrates with counter_click.jinja""" + model = Refilling form_class = RefillForm template_name = "counter/fragments/create_refill.jinja" @@ -242,23 +250,20 @@ class RefillingCreateView(FragmentMixin, FormView): ): raise PermissionDenied - self.operator = get_operator(request, self.counter, self.customer) - return super().dispatch(request, *args, **kwargs) def render_fragment(self, request, **kwargs) -> SafeString: self.customer = kwargs.pop("customer") self.counter = kwargs.pop("counter") + self.object = None return super().render_fragment(request, **kwargs) - def form_valid(self, form): - res = super().form_valid(form) - form.clean() - form.instance.counter = self.counter - form.instance.operator = self.operator - form.instance.customer = self.customer - form.instance.save() - return res + def get_form_kwargs(self): + return super().get_form_kwargs() | { + "counter": self.counter, + "operator": get_operator(self.request, self.counter, self.customer), + "customer": self.customer, + } def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) diff --git a/eboutic/static/bundled/eboutic/eboutic-index.ts b/eboutic/static/bundled/eboutic/eboutic-index.ts index 59a258e3..50a7f715 100644 --- a/eboutic/static/bundled/eboutic/eboutic-index.ts +++ b/eboutic/static/bundled/eboutic/eboutic-index.ts @@ -5,10 +5,11 @@ interface BasketItem { name: string; quantity: number; unitPrice: number; + isRefill: boolean; } const BASKET_CACHE_KEY = "basket"; -const BASKET_CACHE_VERSION = 1; +const BASKET_CACHE_VERSION = 2; document.addEventListener("alpine:init", () => { Alpine.data("basket", (validPrices: number[], lastPurchaseTime?: number) => ({ @@ -21,7 +22,7 @@ document.addEventListener("alpine:init", () => { }); document .getElementById("id_form-TOTAL_FORMS") - .setAttribute(":value", "basket.length"); + ?.setAttribute(":value", "basket.length"); }, loadBasket(): BasketItem[] { @@ -32,8 +33,8 @@ document.addEventListener("alpine:init", () => { return []; } if ( - lastPurchaseTime !== null && - localStorage.basketTimestamp !== undefined && + lastPurchaseTime && + localStorage.basketTimestamp && new Date(lastPurchaseTime) >= new Date(Number.parseInt(localStorage.basketTimestamp, 10)) ) { @@ -64,6 +65,19 @@ document.addEventListener("alpine:init", () => { ); }, + /** + * Get the total of money that would be added to the AE account on basket purchase. + */ + getTotalAdded() { + return this.basket + .filter((item) => item.isRefill || item.unitPrice < 0) + .reduce( + (acc: number, item: BasketItem) => + acc + Math.abs(item.quantity * item.unitPrice), + 0, + ); + }, + /** * Add 1 to the quantity of an item in the basket * @param {BasketItem} item @@ -86,7 +100,7 @@ document.addEventListener("alpine:init", () => { if (this.basket[index].quantity === 0) { this.basket = this.basket.filter( - (e: BasketItem) => e.priceId !== this.basket[index].id, + (e: BasketItem) => e.priceId !== this.basket[index].priceId, ); } }, @@ -103,14 +117,16 @@ document.addEventListener("alpine:init", () => { * @param id The id of the product to add * @param name The name of the product * @param price The unit price of the product + * @param isRefill true if the product is a refill bond * @returns The created item */ - createItem(id: number, name: string, price: number): BasketItem { + createItem(id: number, name: string, price: number, isRefill: boolean): BasketItem { const newItem = { priceId: id, name, quantity: 0, unitPrice: price, + isRefill, } as BasketItem; this.basket.push(newItem); @@ -125,16 +141,17 @@ document.addEventListener("alpine:init", () => { * @param id The id of the product to add * @param name The name of the product * @param price The unit price of the product + * @param isRefill true if the product is a refill bond */ - addFromCatalog(id: number, name: string, price: number) { - let item = this.basket.find((e: BasketItem) => e.priceId === id); + addFromCatalog(id: number, name: string, price: number, isRefill: boolean) { + const item = this.basket.find((e: BasketItem) => e.priceId === id); // if the item is not in the basket, we create it // else we add + 1 to it if (item) { this.add(item); } else { - item = this.createItem(id, name, price); + this.createItem(id, name, price, isRefill); } }, })); diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja index 4962d5ce..78f5429c 100644 --- a/eboutic/templates/eboutic/eboutic_main.jinja +++ b/eboutic/templates/eboutic/eboutic_main.jinja @@ -58,6 +58,17 @@ {% endif %} +