website/hub/services/tests/test_pricing.py

631 lines
23 KiB
Python

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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("50.00"),
)
retrieved_fee = self.price_config.get_base_fee(Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED)
self.assertEqual(retrieved_fee, Decimal("50.00"))
# Test non-existent currency
non_existent_fee = self.price_config.get_base_fee(Currency.EUR, VSHNAppCatPrice.ServiceLevel.GUARANTEED)
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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
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,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
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, service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED, amount=Decimal("25.00")
)
# Test get_price method
price = addon.get_price(Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED)
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, service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED, 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, service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED, 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)