From f6f31af975bd41bee8dd421d85491f8bbf790370 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 7 Jun 2026 14:16:12 +0200 Subject: [PATCH] enforce max amount on sith account --- counter/fields.py | 53 +++++++++++++++++-- ..._customer_amount_alter_refilling_amount.py | 30 +++++++++++ counter/models.py | 18 ++++--- sith/settings.py | 2 + 4 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 counter/migrations/0042_alter_customer_amount_alter_refilling_amount.py diff --git a/counter/fields.py b/counter/fields.py index a212059d..0bde4801 100644 --- a/counter/fields.py +++ b/counter/fields.py @@ -1,22 +1,67 @@ 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): + 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/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..a34f20b1 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( + _("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(_("amount"), min_value=0.01) operator = models.ForeignKey( User, related_name="refillings_as_operator", diff --git a/sith/settings.py b/sith/settings.py index ce872c7d..0081b221 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -503,6 +503,8 @@ SITH_ACCOUNT_INACTIVITY_DELTA = relativedelta(years=2) SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30) """timedelta between the warning mail and the actual account dump""" +SITH_ACCOUNT_MAX_MONEY = 250 # € + # Defines which product type is the refilling type, # and thus increases the account amount SITH_COUNTER_PRODUCTTYPE_REFILLING = env.int(