325 lines
11 KiB
Python
325 lines
11 KiB
Python
from decimal import Decimal
|
|
from django.test import TestCase
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
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,
|
|
ExternalPricePlans,
|
|
)
|
|
|
|
|
|
class PricingEdgeCasesTestCase(TestCase):
|
|
"""Test edge cases and error conditions in pricing models"""
|
|
|
|
def setUp(self):
|
|
self.cloud_provider = CloudProvider.objects.create(
|
|
name="Test Provider",
|
|
slug="test-provider",
|
|
description="Test cloud provider",
|
|
website="https://test.com",
|
|
)
|
|
|
|
self.service = Service.objects.create(
|
|
name="Test Service",
|
|
slug="test-service",
|
|
description="Test service",
|
|
features="Test features",
|
|
)
|
|
|
|
def test_discount_tier_validation_overlapping_ranges(self):
|
|
"""Test that overlapping discount tiers can be created (business logic may handle conflicts)"""
|
|
discount_model = ProgressiveDiscountModel.objects.create(
|
|
name="Test Discount", active=True
|
|
)
|
|
|
|
# Create first tier
|
|
DiscountTier.objects.create(
|
|
discount_model=discount_model,
|
|
min_units=0,
|
|
max_units=20,
|
|
discount_percent=Decimal("5.00"),
|
|
)
|
|
|
|
# Create overlapping tier - should be allowed at model level
|
|
# Business logic in calculate_discount should handle this appropriately
|
|
DiscountTier.objects.create(
|
|
discount_model=discount_model,
|
|
min_units=15,
|
|
max_units=30,
|
|
discount_percent=Decimal("10.00"),
|
|
)
|
|
|
|
# Should be able to create both tiers
|
|
self.assertEqual(discount_model.tiers.count(), 2)
|
|
|
|
def test_discount_calculation_with_zero_units(self):
|
|
"""Test discount calculation with zero units"""
|
|
discount_model = ProgressiveDiscountModel.objects.create(
|
|
name="Test Discount", active=True
|
|
)
|
|
|
|
DiscountTier.objects.create(
|
|
discount_model=discount_model,
|
|
min_units=0,
|
|
max_units=10,
|
|
discount_percent=Decimal("0.00"),
|
|
)
|
|
|
|
result = discount_model.calculate_discount(Decimal("10.00"), 0)
|
|
self.assertEqual(result, 0)
|
|
|
|
def test_discount_calculation_with_very_large_numbers(self):
|
|
"""Test discount calculation with very large numbers"""
|
|
discount_model = ProgressiveDiscountModel.objects.create(
|
|
name="Test Discount", active=True
|
|
)
|
|
|
|
DiscountTier.objects.create(
|
|
discount_model=discount_model,
|
|
min_units=0,
|
|
max_units=None,
|
|
discount_percent=Decimal("10.00"),
|
|
)
|
|
|
|
large_units = 1000000
|
|
base_rate = Decimal("0.001")
|
|
|
|
result = discount_model.calculate_discount(base_rate, large_units)
|
|
expected = base_rate * Decimal("0.9") * large_units
|
|
self.assertEqual(result, expected)
|
|
|
|
def test_price_calculation_with_inactive_discount_model(self):
|
|
"""Test price calculation when discount model is inactive"""
|
|
discount_model = ProgressiveDiscountModel.objects.create(
|
|
name="Inactive Discount", active=False # Inactive
|
|
)
|
|
|
|
DiscountTier.objects.create(
|
|
discount_model=discount_model,
|
|
min_units=0,
|
|
max_units=None,
|
|
discount_percent=Decimal("50.00"), # Large discount
|
|
)
|
|
|
|
price_config = VSHNAppCatPrice.objects.create(
|
|
service=self.service,
|
|
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
|
|
term=Term.MTH,
|
|
discount_model=discount_model,
|
|
)
|
|
|
|
VSHNAppCatBaseFee.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
currency=Currency.CHF,
|
|
amount=Decimal("50.00"),
|
|
)
|
|
|
|
VSHNAppCatUnitRate.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
currency=Currency.CHF,
|
|
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
|
|
amount=Decimal("10.0000"),
|
|
)
|
|
|
|
result = price_config.calculate_final_price(
|
|
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 5
|
|
)
|
|
|
|
# Should ignore discount since model is inactive
|
|
expected_total = Decimal("50.00") + (Decimal("10.0000") * 5)
|
|
self.assertEqual(result["total_price"], expected_total)
|
|
|
|
def test_addon_with_missing_prices(self):
|
|
"""Test addon behavior when price data is missing"""
|
|
price_config = VSHNAppCatPrice.objects.create(
|
|
service=self.service,
|
|
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
|
|
term=Term.MTH,
|
|
)
|
|
|
|
# Create addon but no corresponding price objects
|
|
addon = VSHNAppCatAddon.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
name="Incomplete Addon",
|
|
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
|
|
mandatory=True,
|
|
)
|
|
|
|
# Should return None when price doesn't exist
|
|
price = addon.get_price(Currency.CHF)
|
|
self.assertIsNone(price)
|
|
|
|
def test_compute_plan_with_validity_dates(self):
|
|
"""Test compute plan with validity date ranges"""
|
|
now = timezone.now()
|
|
past_date = now - timedelta(days=30)
|
|
future_date = now + timedelta(days=30)
|
|
|
|
# Create plan that is not yet valid
|
|
future_plan = ComputePlan.objects.create(
|
|
name="Future Plan",
|
|
vcpus=4.0,
|
|
ram=8.0,
|
|
cpu_mem_ratio=0.5,
|
|
cloud_provider=self.cloud_provider,
|
|
valid_from=future_date,
|
|
valid_to=None,
|
|
)
|
|
|
|
# Create plan that has expired
|
|
expired_plan = ComputePlan.objects.create(
|
|
name="Expired Plan",
|
|
vcpus=2.0,
|
|
ram=4.0,
|
|
cpu_mem_ratio=0.5,
|
|
cloud_provider=self.cloud_provider,
|
|
valid_from=past_date,
|
|
valid_to=past_date + timedelta(days=10),
|
|
)
|
|
|
|
# Plans should still be created and queryable
|
|
self.assertTrue(ComputePlan.objects.filter(name="Future Plan").exists())
|
|
self.assertTrue(ComputePlan.objects.filter(name="Expired Plan").exists())
|
|
|
|
def test_decimal_precision_edge_cases(self):
|
|
"""Test decimal precision in price calculations"""
|
|
price_config = VSHNAppCatPrice.objects.create(
|
|
service=self.service,
|
|
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
|
|
term=Term.MTH,
|
|
)
|
|
|
|
VSHNAppCatBaseFee.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
currency=Currency.CHF,
|
|
amount=Decimal("0.01"), # Very small base fee
|
|
)
|
|
|
|
VSHNAppCatUnitRate.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
currency=Currency.CHF,
|
|
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
|
|
amount=Decimal("0.0001"), # Very small unit rate
|
|
)
|
|
|
|
result = price_config.calculate_final_price(
|
|
Currency.CHF,
|
|
VSHNAppCatPrice.ServiceLevel.GUARANTEED,
|
|
1000, # Large quantity
|
|
)
|
|
|
|
expected_total = Decimal("0.01") + (Decimal("0.0001") * 1000)
|
|
self.assertEqual(result["total_price"], expected_total)
|
|
|
|
def test_external_price_comparison_model(self):
|
|
"""Test ExternalPricePlans model functionality"""
|
|
compute_plan = ComputePlan.objects.create(
|
|
name="Comparison Plan",
|
|
vcpus=2.0,
|
|
ram=4.0,
|
|
cpu_mem_ratio=0.5,
|
|
cloud_provider=self.cloud_provider,
|
|
)
|
|
|
|
external_price = ExternalPricePlans.objects.create(
|
|
plan_name="Competitor Plan",
|
|
description="Competitor offering",
|
|
cloud_provider=self.cloud_provider,
|
|
service=self.service,
|
|
currency=Currency.USD,
|
|
term=Term.MTH,
|
|
amount=Decimal("99.99"),
|
|
vcpus=2.0,
|
|
ram=4.0,
|
|
storage=50.0,
|
|
)
|
|
|
|
external_price.compare_to.add(compute_plan)
|
|
|
|
self.assertEqual(
|
|
str(external_price), "Test Provider - Test Service - Competitor Plan"
|
|
)
|
|
self.assertTrue(external_price.compare_to.filter(id=compute_plan.id).exists())
|
|
|
|
def test_unique_constraints_enforcement(self):
|
|
"""Test that unique constraints are properly enforced"""
|
|
compute_plan = ComputePlan.objects.create(
|
|
name="Test Plan",
|
|
vcpus=2.0,
|
|
ram=4.0,
|
|
cpu_mem_ratio=0.5,
|
|
cloud_provider=self.cloud_provider,
|
|
)
|
|
|
|
# Create first price
|
|
ComputePlanPrice.objects.create(
|
|
compute_plan=compute_plan, currency=Currency.CHF, amount=Decimal("100.00")
|
|
)
|
|
|
|
# Try to create duplicate - should raise IntegrityError
|
|
with self.assertRaises(Exception): # IntegrityError or ValidationError
|
|
ComputePlanPrice.objects.create(
|
|
compute_plan=compute_plan,
|
|
currency=Currency.CHF,
|
|
amount=Decimal("200.00"),
|
|
)
|
|
|
|
def test_addon_ordering_and_active_filtering(self):
|
|
"""Test addon ordering and active status filtering"""
|
|
price_config = VSHNAppCatPrice.objects.create(
|
|
service=self.service,
|
|
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
|
|
term=Term.MTH,
|
|
)
|
|
|
|
# Create addons with different orders and active status
|
|
addon1 = VSHNAppCatAddon.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
name="Third Addon",
|
|
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
|
|
order=3,
|
|
active=True,
|
|
)
|
|
|
|
addon2 = VSHNAppCatAddon.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
name="First Addon",
|
|
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
|
|
order=1,
|
|
active=True,
|
|
)
|
|
|
|
addon3 = VSHNAppCatAddon.objects.create(
|
|
vshn_appcat_price_config=price_config,
|
|
name="Inactive Addon",
|
|
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
|
|
order=2,
|
|
active=False,
|
|
)
|
|
|
|
# Test ordering
|
|
ordered_addons = price_config.addons.all().order_by("order", "name")
|
|
self.assertEqual(ordered_addons[0], addon2) # order=1
|
|
self.assertEqual(ordered_addons[1], addon3) # order=2
|
|
self.assertEqual(ordered_addons[2], addon1) # order=3
|
|
|
|
# Test active filtering
|
|
active_addons = price_config.addons.filter(active=True)
|
|
self.assertEqual(active_addons.count(), 2)
|
|
self.assertNotIn(addon3, active_addons)
|