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)