add addons to services

This commit is contained in:
Tobias Brunner 2025-06-19 16:19:59 +02:00
parent b96b186875
commit 22e527bcd9
No known key found for this signature in database
8 changed files with 1039 additions and 4 deletions

View file

@ -1,4 +1,5 @@
from django.db import models
from django.db.models import Q
from .base import Currency, Term, Unit
from .providers import CloudProvider
@ -339,7 +340,11 @@ class VSHNAppCatPrice(models.Model):
return None
def calculate_final_price(
self, currency_code: str, service_level: str, number_of_units: int
self,
currency_code: str,
service_level: str,
number_of_units: int,
addon_ids=None,
):
base_fee = self.get_base_fee(currency_code)
unit_rate = self.get_unit_rate(currency_code, service_level)
@ -359,7 +364,49 @@ class VSHNAppCatPrice(models.Model):
else:
total_price = base_fee + (unit_rate * number_of_units)
return total_price
# Add prices for mandatory addons and selected addons
addon_total = 0
addon_breakdown = []
# Query all active addons related to this price config
addons_query = self.addons.filter(active=True)
# Include mandatory addons and explicitly selected addons
if addon_ids:
addons = addons_query.filter(Q(mandatory=True) | Q(id__in=addon_ids))
else:
addons = addons_query.filter(mandatory=True)
for addon in addons:
addon_price = 0
if addon.addon_type == VSHNAppCatAddon.AddonType.BASE_FEE:
addon_price_value = addon.get_price(currency_code)
if addon_price_value:
addon_price = addon_price_value
elif addon.addon_type == VSHNAppCatAddon.AddonType.UNIT_RATE:
addon_price_value = addon.get_price(currency_code, service_level)
if addon_price_value:
addon_price = addon_price_value * number_of_units
addon_total += addon_price
addon_breakdown.append(
{
"id": addon.id,
"name": addon.name,
"description": addon.description,
"commercial_description": addon.commercial_description,
"mandatory": addon.mandatory,
"price": addon_price,
}
)
total_price += addon_total
return {
"total_price": total_price,
"addon_total": addon_total,
"addon_breakdown": addon_breakdown,
}
class VSHNAppCatUnitRate(models.Model):
@ -389,6 +436,118 @@ class VSHNAppCatUnitRate(models.Model):
return f"{self.vshn_appcat_price_config.service.name} - {self.get_service_level_display()} Unit Rate - {self.amount} {self.currency}"
class VSHNAppCatAddon(models.Model):
"""
Addon pricing model for VSHNAppCatPrice. Can be added to a service price configuration
to provide additional features or resources with their own pricing.
"""
class AddonType(models.TextChoices):
BASE_FEE = "BF", "Base Fee" # Fixed amount regardless of units
UNIT_RATE = "UR", "Unit Rate" # Price per unit
vshn_appcat_price_config = models.ForeignKey(
VSHNAppCatPrice, on_delete=models.CASCADE, related_name="addons"
)
name = models.CharField(max_length=100, help_text="Name of the addon")
description = models.TextField(
blank=True, help_text="Technical description of the addon"
)
commercial_description = models.TextField(
blank=True, help_text="Commercial description displayed in the frontend"
)
addon_type = models.CharField(
max_length=2,
choices=AddonType.choices,
help_text="Type of addon pricing (fixed fee or per-unit)",
)
mandatory = models.BooleanField(default=False, help_text="Is this addon mandatory?")
active = models.BooleanField(
default=True, help_text="Is this addon active and available for selection?"
)
order = models.IntegerField(default=0, help_text="Display order in the frontend")
valid_from = models.DateTimeField(blank=True, null=True)
valid_to = models.DateTimeField(blank=True, null=True)
class Meta:
verbose_name = "Service Addon"
ordering = ["order", "name"]
def __str__(self):
return f"{self.vshn_appcat_price_config.service.name} - {self.name}"
def get_price(self, currency_code: str, service_level: str = None):
"""Get the price for this addon in the specified currency and service level"""
try:
if self.addon_type == self.AddonType.BASE_FEE:
return self.base_fees.get(currency=currency_code).amount
elif self.addon_type == self.AddonType.UNIT_RATE:
if not service_level:
raise ValueError("Service level is required for unit rate addons")
return self.unit_rates.get(
currency=currency_code, service_level=service_level
).amount
except (
VSHNAppCatAddonBaseFee.DoesNotExist,
VSHNAppCatAddonUnitRate.DoesNotExist,
):
return None
class VSHNAppCatAddonBaseFee(models.Model):
"""Base fee for an addon (fixed amount regardless of units)"""
addon = models.ForeignKey(
VSHNAppCatAddon, on_delete=models.CASCADE, related_name="base_fees"
)
currency = models.CharField(
max_length=3,
choices=Currency.choices,
)
amount = models.DecimalField(
max_digits=10,
decimal_places=2,
help_text="Base fee in the specified currency, excl. VAT",
)
class Meta:
verbose_name = "Addon Base Fee"
unique_together = ("addon", "currency")
ordering = ["currency"]
def __str__(self):
return f"{self.addon.name} Base Fee - {self.amount} {self.currency}"
class VSHNAppCatAddonUnitRate(models.Model):
"""Unit rate for an addon (price per unit)"""
addon = models.ForeignKey(
VSHNAppCatAddon, on_delete=models.CASCADE, related_name="unit_rates"
)
currency = models.CharField(
max_length=3,
choices=Currency.choices,
)
service_level = models.CharField(
max_length=2,
choices=VSHNAppCatPrice.ServiceLevel.choices,
)
amount = models.DecimalField(
max_digits=10,
decimal_places=4,
help_text="Price per unit in the specified currency and service level, excl. VAT",
)
class Meta:
verbose_name = "Addon Unit Rate"
unique_together = ("addon", "currency", "service_level")
ordering = ["currency", "service_level"]
def __str__(self):
return f"{self.addon.name} - {self.get_service_level_display()} Unit Rate - {self.amount} {self.currency}"
class ExternalPricePlans(models.Model):
plan_name = models.CharField()
description = models.CharField(max_length=200, blank=True, null=True)