website/hub/services/tests/test_pricing.py

625 lines
22 KiB
Python
Raw Normal View History

2025-06-20 10:46:11 +02:00
from decimal import Decimal
from django.test import TestCase
from django.core.exceptions import ValidationError
from ..models.base import Currency, Term, Unit
from ..models.providers import CloudProvider
from ..models.services import Service
from ..models.pricing import (
ComputePlan,
ComputePlanPrice,
StoragePlan,
StoragePlanPrice,
ProgressiveDiscountModel,
DiscountTier,
VSHNAppCatPrice,
VSHNAppCatBaseFee,
VSHNAppCatUnitRate,
VSHNAppCatAddon,
VSHNAppCatAddonBaseFee,
VSHNAppCatAddonUnitRate,
)
class ComputePlanTestCase(TestCase):
"""Test cases for ComputePlan model and pricing"""
def setUp(self):
# Create test cloud provider
self.cloud_provider = CloudProvider.objects.create(
name="Test Provider",
slug="test-provider",
description="Test cloud provider",
website="https://test.com",
)
# Create test compute plan
self.compute_plan = ComputePlan.objects.create(
name="Small Plan",
vcpus=2.0,
ram=4.0,
cpu_mem_ratio=0.5,
cloud_provider=self.cloud_provider,
term=Term.MTH,
)
def test_compute_plan_str_representation(self):
"""Test string representation of ComputePlan"""
self.assertEqual(str(self.compute_plan), "Small Plan")
def test_compute_plan_price_creation(self):
"""Test creation of ComputePlanPrice"""
price = ComputePlanPrice.objects.create(
compute_plan=self.compute_plan,
currency=Currency.CHF,
amount=Decimal("100.00"),
)
self.assertEqual(price.amount, Decimal("100.00"))
self.assertEqual(price.currency, Currency.CHF)
def test_compute_plan_get_price_exists(self):
"""Test get_price method when price exists"""
ComputePlanPrice.objects.create(
compute_plan=self.compute_plan,
currency=Currency.CHF,
amount=Decimal("100.00"),
)
price = self.compute_plan.get_price(Currency.CHF)
self.assertEqual(price, Decimal("100.00"))
def test_compute_plan_get_price_not_exists(self):
"""Test get_price method when price doesn't exist"""
price = self.compute_plan.get_price(Currency.EUR)
self.assertIsNone(price)
def test_compute_plan_price_unique_constraint(self):
"""Test unique constraint on compute_plan and currency"""
ComputePlanPrice.objects.create(
compute_plan=self.compute_plan,
currency=Currency.CHF,
amount=Decimal("100.00"),
)
# Creating another price with same plan and currency should be allowed
# (this will update the existing one)
with self.assertRaises(Exception):
ComputePlanPrice.objects.create(
compute_plan=self.compute_plan,
currency=Currency.CHF,
amount=Decimal("200.00"),
)
class StoragePlanTestCase(TestCase):
"""Test cases for StoragePlan model and pricing"""
def setUp(self):
# Create test cloud provider
self.cloud_provider = CloudProvider.objects.create(
name="Test Provider",
slug="test-provider",
description="Test cloud provider",
website="https://test.com",
)
# Create test storage plan
self.storage_plan = StoragePlan.objects.create(
name="SSD Storage",
cloud_provider=self.cloud_provider,
term=Term.MTH,
unit=Unit.GIB,
)
def test_storage_plan_str_representation(self):
"""Test string representation of StoragePlan"""
self.assertEqual(str(self.storage_plan), "SSD Storage")
def test_storage_plan_price_creation(self):
"""Test creation of StoragePlanPrice"""
price = StoragePlanPrice.objects.create(
storage_plan=self.storage_plan,
currency=Currency.CHF,
amount=Decimal("0.15"),
)
self.assertEqual(price.amount, Decimal("0.15"))
self.assertEqual(price.currency, Currency.CHF)
def test_storage_plan_get_price_exists(self):
"""Test get_price method when price exists"""
StoragePlanPrice.objects.create(
storage_plan=self.storage_plan,
currency=Currency.USD,
amount=Decimal("0.12"),
)
price = self.storage_plan.get_price(Currency.USD)
self.assertEqual(price, Decimal("0.12"))
def test_storage_plan_get_price_not_exists(self):
"""Test get_price method when price doesn't exist"""
price = self.storage_plan.get_price(Currency.EUR)
self.assertIsNone(price)
class ProgressiveDiscountModelTestCase(TestCase):
"""Test cases for ProgressiveDiscountModel and discount calculations"""
def setUp(self):
# Create discount model
self.discount_model = ProgressiveDiscountModel.objects.create(
name="Volume Discount",
description="Progressive discount based on usage",
active=True,
)
# Create discount tiers
# 0-9 units: 0% discount
DiscountTier.objects.create(
discount_model=self.discount_model,
min_units=0,
max_units=10,
discount_percent=Decimal("0.00"),
)
# 10-49 units: 10% discount
DiscountTier.objects.create(
discount_model=self.discount_model,
min_units=10,
max_units=50,
discount_percent=Decimal("10.00"),
)
# 50+ units: 20% discount
DiscountTier.objects.create(
discount_model=self.discount_model,
min_units=50,
max_units=None, # Unlimited
discount_percent=Decimal("20.00"),
)
def test_discount_model_str_representation(self):
"""Test string representation of DiscountModel"""
self.assertEqual(str(self.discount_model), "Volume Discount")
def test_calculate_discount_no_discount_tier(self):
"""Test discount calculation for units in first tier (no discount)"""
base_rate = Decimal("10.00")
units = 5
final_price = self.discount_model.calculate_discount(base_rate, units)
expected_price = base_rate * units # No discount
self.assertEqual(final_price, expected_price)
def test_calculate_discount_single_tier(self):
"""Test discount calculation for units in second tier"""
base_rate = Decimal("10.00")
units = 15
final_price = self.discount_model.calculate_discount(base_rate, units)
# First 10 units at full price, next 5 at 90% (10% discount)
expected_price = (base_rate * 10) + (base_rate * Decimal("0.9") * 5)
self.assertEqual(final_price, expected_price)
def test_calculate_discount_multiple_tiers(self):
"""Test discount calculation spanning multiple tiers"""
base_rate = Decimal("10.00")
units = 60
final_price = self.discount_model.calculate_discount(base_rate, units)
# First 10 units at 100% (0% discount)
# Next 40 units at 90% (10% discount)
# Next 10 units at 80% (20% discount)
expected_price = (
(base_rate * 10)
+ (base_rate * Decimal("0.9") * 40)
+ (base_rate * Decimal("0.8") * 10)
)
self.assertEqual(final_price, expected_price)
def test_get_discount_breakdown(self):
"""Test discount breakdown calculation"""
base_rate = Decimal("10.00")
units = 25
breakdown = self.discount_model.get_discount_breakdown(base_rate, units)
self.assertEqual(len(breakdown), 2) # Should have 2 tiers
# First tier: 0-9 units at 0% discount
self.assertEqual(breakdown[0]["units"], 10)
self.assertEqual(breakdown[0]["discount_percent"], Decimal("0.00"))
self.assertEqual(breakdown[0]["rate"], base_rate)
# Second tier: 10-24 units at 10% discount
self.assertEqual(breakdown[1]["units"], 15)
self.assertEqual(breakdown[1]["discount_percent"], Decimal("10.00"))
self.assertEqual(breakdown[1]["rate"], base_rate * Decimal("0.9"))
def test_discount_tier_str_representation(self):
"""Test string representation of DiscountTier"""
tier = self.discount_model.tiers.get(min_units=10, max_units=50)
expected_str = "Volume Discount: 10-49 units → 10.00% discount"
self.assertEqual(str(tier), expected_str)
# Test unlimited tier
tier_unlimited = self.discount_model.tiers.get(min_units=50, max_units=None)
expected_str = "Volume Discount: 50+ units → 20.00% discount"
self.assertEqual(str(tier_unlimited), expected_str)
class VSHNAppCatPriceTestCase(TestCase):
"""Test cases for VSHNAppCatPrice model and final price calculations"""
def setUp(self):
# Create test service
self.service = Service.objects.create(
name="PostgreSQL",
slug="postgresql",
description="PostgreSQL database service",
features="High availability, automated backups",
)
# Create discount model
self.discount_model = ProgressiveDiscountModel.objects.create(
name="RAM Discount",
description="Progressive discount for RAM usage",
active=True,
)
# Create discount tiers
DiscountTier.objects.create(
discount_model=self.discount_model,
min_units=0,
max_units=5,
discount_percent=Decimal("0.00"),
)
DiscountTier.objects.create(
discount_model=self.discount_model,
min_units=5,
max_units=None,
discount_percent=Decimal("10.00"),
)
# Create price configuration
self.price_config = VSHNAppCatPrice.objects.create(
service=self.service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
discount_model=self.discount_model,
)
def test_vshn_appcat_price_str_representation(self):
"""Test string representation of VSHNAppCatPrice"""
expected_str = "PostgreSQL - Memory (RAM) based pricing"
self.assertEqual(str(self.price_config), expected_str)
def test_base_fee_creation_and_retrieval(self):
"""Test creation and retrieval of base fees"""
base_fee = VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
retrieved_fee = self.price_config.get_base_fee(Currency.CHF)
self.assertEqual(retrieved_fee, Decimal("50.00"))
# Test non-existent currency
non_existent_fee = self.price_config.get_base_fee(Currency.EUR)
self.assertIsNone(non_existent_fee)
def test_unit_rate_creation_and_retrieval(self):
"""Test creation and retrieval of unit rates"""
unit_rate = VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("5.0000"),
)
retrieved_rate = self.price_config.get_unit_rate(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED
)
self.assertEqual(retrieved_rate, Decimal("5.0000"))
# Test non-existent combination
non_existent_rate = self.price_config.get_unit_rate(
Currency.EUR, VSHNAppCatPrice.ServiceLevel.GUARANTEED
)
self.assertIsNone(non_existent_rate)
def test_calculate_final_price_simple(self):
"""Test final price calculation without addons"""
# Create base fee and unit rate
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("5.0000"),
)
# Calculate price for 3 GiB RAM (should be in first tier, no discount)
result = self.price_config.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 3
)
expected_total = Decimal("50.00") + (Decimal("5.0000") * 3)
self.assertEqual(result["total_price"], expected_total)
self.assertEqual(result["addon_total"], 0)
def test_calculate_final_price_with_discount(self):
"""Test final price calculation with progressive discount"""
# Create base fee and unit rate
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("5.0000"),
)
# Calculate price for 8 GiB RAM (spans two discount tiers)
result = self.price_config.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 8
)
# First 5 units at full price, next 3 at 90% (10% discount)
discounted_price = (Decimal("5.0000") * 5) + (
Decimal("5.0000") * Decimal("0.9") * 3
)
expected_total = Decimal("50.00") + discounted_price
self.assertEqual(result["total_price"], expected_total)
def test_calculate_final_price_negative_units(self):
"""Test final price calculation with negative units (should raise error)"""
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("5.0000"),
)
with self.assertRaises(ValueError):
self.price_config.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, -1
)
def test_calculate_final_price_missing_data(self):
"""Test final price calculation when base fee or unit rate is missing"""
# Only create base fee, no unit rate
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
result = self.price_config.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 3
)
self.assertIsNone(result)
def test_calculate_final_price_without_discount_model(self):
"""Test final price calculation when no discount model is active"""
# Remove discount model
self.price_config.discount_model = None
self.price_config.save()
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("5.0000"),
)
result = self.price_config.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 8
)
# Should be simple multiplication without discount
expected_total = Decimal("50.00") + (Decimal("5.0000") * 8)
self.assertEqual(result["total_price"], expected_total)
class VSHNAppCatAddonTestCase(TestCase):
"""Test cases for VSHNAppCat addons and pricing"""
def setUp(self):
# Create test service
self.service = Service.objects.create(
name="PostgreSQL",
slug="postgresql",
description="PostgreSQL database service",
features="High availability, automated backups",
)
# Create price configuration
self.price_config = VSHNAppCatPrice.objects.create(
service=self.service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
)
# Create base fee and unit rate for main service
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
amount=Decimal("50.00"),
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=self.price_config,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("5.0000"),
)
def test_addon_base_fee_type(self):
"""Test addon with base fee type"""
addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="Backup Extension",
description="Extended backup retention",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
mandatory=True,
)
# Create base fee for addon
VSHNAppCatAddonBaseFee.objects.create(
addon=addon, currency=Currency.CHF, amount=Decimal("25.00")
)
# Test get_price method
price = addon.get_price(Currency.CHF)
self.assertEqual(price, Decimal("25.00"))
def test_addon_unit_rate_type(self):
"""Test addon with unit rate type"""
addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="Additional Storage",
description="Extra storage per GiB",
addon_type=VSHNAppCatAddon.AddonType.UNIT_RATE,
mandatory=False,
)
# Create unit rate for addon
VSHNAppCatAddonUnitRate.objects.create(
addon=addon,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("0.5000"),
)
# Test get_price method
price = addon.get_price(Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED)
self.assertEqual(price, Decimal("0.5000"))
def test_addon_unit_rate_without_service_level(self):
"""Test addon unit rate without service level (should raise error)"""
addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="Additional Storage",
addon_type=VSHNAppCatAddon.AddonType.UNIT_RATE,
)
VSHNAppCatAddonUnitRate.objects.create(
addon=addon,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("0.5000"),
)
with self.assertRaises(ValueError):
addon.get_price(Currency.CHF)
def test_calculate_final_price_with_mandatory_addons(self):
"""Test final price calculation including mandatory addons"""
# Create mandatory base fee addon
mandatory_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="Backup Extension",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
mandatory=True,
)
VSHNAppCatAddonBaseFee.objects.create(
addon=mandatory_addon, currency=Currency.CHF, amount=Decimal("25.00")
)
# Create mandatory unit rate addon
mandatory_unit_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="Monitoring",
addon_type=VSHNAppCatAddon.AddonType.UNIT_RATE,
mandatory=True,
)
VSHNAppCatAddonUnitRate.objects.create(
addon=mandatory_unit_addon,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("1.0000"),
)
result = self.price_config.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 4
)
# Base: 50 + (5 * 4) = 70
# Mandatory addons: 25 + (1 * 4) = 29
# Total: 99
expected_total = Decimal("99.00")
self.assertEqual(result["total_price"], expected_total)
self.assertEqual(result["addon_total"], Decimal("29.00"))
self.assertEqual(len(result["addon_breakdown"]), 2)
def test_calculate_final_price_with_selected_addons(self):
"""Test final price calculation including selected optional addons"""
# Create optional addon
optional_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="SSL Certificate",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
mandatory=False,
)
VSHNAppCatAddonBaseFee.objects.create(
addon=optional_addon, currency=Currency.CHF, amount=Decimal("15.00")
)
# Calculate price with selected addon
result = self.price_config.calculate_final_price(
Currency.CHF,
VSHNAppCatPrice.ServiceLevel.GUARANTEED,
4,
addon_ids=[optional_addon.id],
)
# Base: 50 + (5 * 4) = 70
# Selected addon: 15
# Total: 85
expected_total = Decimal("85.00")
self.assertEqual(result["total_price"], expected_total)
self.assertEqual(result["addon_total"], Decimal("15.00"))
def test_addon_str_representation(self):
"""Test string representation of addon"""
addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=self.price_config,
name="Backup Extension",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
)
expected_str = "PostgreSQL - Backup Extension"
self.assertEqual(str(addon), expected_str)