From 836187f2aa49742493799bd7723f5e77ef4923a7 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 22 May 2025 16:52:34 +0200 Subject: [PATCH] add discount models --- hub/services/admin.py | 19 +++- ...del_vshnappcatprice_valid_from_and_more.py | 95 +++++++++++++++++++ hub/services/models.py | 83 +++++++++++++++- 3 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 hub/services/migrations/0026_progressivediscountmodel_vshnappcatprice_valid_from_and_more.py diff --git a/hub/services/admin.py b/hub/services/admin.py index 5a9aafb..4cb8afc 100644 --- a/hub/services/admin.py +++ b/hub/services/admin.py @@ -16,6 +16,8 @@ from .models import ( ExternalLinkOffering, Lead, Plan, + ProgressiveDiscountModel, + DiscountTier, ReusableText, Service, ServiceOffering, @@ -292,16 +294,31 @@ class VSHNAppCatUnitRateInline(admin.TabularInline): fields = ("currency", "service_level", "amount") +class DiscountTierInline(admin.TabularInline): + model = DiscountTier + extra = 1 + fields = ("threshold", "discount_percent") + ordering = ("threshold",) + + +@admin.register(ProgressiveDiscountModel) +class ProgressiveDiscountModelAdmin(admin.ModelAdmin): + list_display = ("name", "description", "active") + search_fields = ("name", "description") + inlines = [DiscountTierInline] + + @admin.register(VSHNAppCatPrice) class VSHNAppCatPriceAdmin(admin.ModelAdmin): list_display = ( "service", "variable_unit", "term", + "discount_model", "admin_display_base_fees", "admin_display_unit_rates", ) - list_filter = ("variable_unit", "service") + list_filter = ("variable_unit", "service", "discount_model") search_fields = ("service__name",) inlines = [VSHNAppCatBaseFeeInline, VSHNAppCatUnitRateInline] diff --git a/hub/services/migrations/0026_progressivediscountmodel_vshnappcatprice_valid_from_and_more.py b/hub/services/migrations/0026_progressivediscountmodel_vshnappcatprice_valid_from_and_more.py new file mode 100644 index 0000000..06af61f --- /dev/null +++ b/hub/services/migrations/0026_progressivediscountmodel_vshnappcatprice_valid_from_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 5.2 on 2025-05-22 14:50 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "services", + "0025_computeplan_term_storageplan_term_storageplan_unit_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="ProgressiveDiscountModel", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField(blank=True)), + ("active", models.BooleanField(default=True)), + ], + ), + migrations.AddField( + model_name="vshnappcatprice", + name="valid_from", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="vshnappcatprice", + name="valid_to", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="vshnappcatprice", + name="discount_model", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="price_configs", + to="services.progressivediscountmodel", + ), + ), + migrations.CreateModel( + name="DiscountTier", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "threshold", + models.PositiveIntegerField( + help_text="Starting unit count for this tier" + ), + ), + ( + "discount_percent", + models.DecimalField( + decimal_places=2, + help_text="Percentage discount applied (0-100)", + max_digits=5, + ), + ), + ( + "discount_model", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tiers", + to="services.progressivediscountmodel", + ), + ), + ], + options={ + "ordering": ["threshold"], + "unique_together": {("discount_model", "threshold")}, + }, + ), + ] diff --git a/hub/services/models.py b/hub/services/models.py index acb34d0..e6c6478 100644 --- a/hub/services/models.py +++ b/hub/services/models.py @@ -508,6 +508,68 @@ class VSHNAppCatBaseFee(models.Model): return f"{self.vshn_appcat_price_config.service.name} Base Fee - {self.amount} {self.currency}" +class ProgressiveDiscountModel(models.Model): + name = models.CharField(max_length=100) + description = models.TextField(blank=True) + active = models.BooleanField(default=True) + + def __str__(self): + return self.name + + def calculate_discount(self, base_rate, units): + """Calculate price using progressive percentage discounts.""" + final_price = 0 + remaining_units = units + processed_units = 0 + + # Get all tiers sorted by threshold + discount_tiers = self.tiers.all().order_by("threshold") + + for i, tier in enumerate(discount_tiers): + # Calculate how many units fall into this tier + if i < discount_tiers.count() - 1: + next_threshold = discount_tiers[i + 1].threshold + tier_units = min(remaining_units, next_threshold - processed_units) + else: + # Last tier handles all remaining units + tier_units = remaining_units + + # Calculate discounted rate for this tier + discounted_rate = base_rate * (1 - tier.discount_percent / 100) + + # Add the price for units in this tier + final_price += discounted_rate * tier_units + + # Update tracking variables + remaining_units -= tier_units + processed_units += tier_units + + # Exit if all units have been processed + if remaining_units <= 0: + break + + return final_price + + +class DiscountTier(models.Model): + discount_model = models.ForeignKey( + ProgressiveDiscountModel, on_delete=models.CASCADE, related_name="tiers" + ) + threshold = models.PositiveIntegerField( + help_text="Starting unit count for this tier" + ) + discount_percent = models.DecimalField( + max_digits=5, decimal_places=2, help_text="Percentage discount applied (0-100)" + ) + + class Meta: + ordering = ["threshold"] + unique_together = ["discount_model", "threshold"] + + def __str__(self): + return f"{self.discount_model.name}: {self.threshold}+ units → {self.discount_percent}% discount" + + class VSHNAppCatPrice(models.Model): class VariableUnit(models.TextChoices): RAM = "RAM", "Memory (RAM)" @@ -538,6 +600,17 @@ class VSHNAppCatPrice(models.Model): choices=Term.choices, ) + valid_from = models.DateTimeField(blank=True, null=True) + valid_to = models.DateTimeField(blank=True, null=True) + + discount_model = models.ForeignKey( + ProgressiveDiscountModel, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="price_configs", + ) + def __str__(self): return f"{self.service.name} - {self.get_variable_unit_display()} based pricing" @@ -567,7 +640,15 @@ class VSHNAppCatPrice(models.Model): if number_of_units < 0: raise ValueError("Number of units cannot be negative") - total_price = base_fee + (unit_rate * number_of_units) + # Apply discount model if available + if self.discount_model and self.discount_model.active: + discounted_price = self.discount_model.calculate_discount( + unit_rate, number_of_units + ) + total_price = base_fee + discounted_price + else: + total_price = base_fee + (unit_rate * number_of_units) + return total_price