tests and actions
Some checks failed
Pricing Tests / Pricing Model Tests (push) Failing after 4s
Pricing Tests / Pricing Documentation Check (push) Failing after 3s

This commit is contained in:
Tobias Brunner 2025-06-20 10:46:11 +02:00
parent c05feb37d3
commit 78f52ea7f4
No known key found for this signature in database
17 changed files with 4140 additions and 3 deletions

View file

@ -1,3 +1,4 @@
from django.test import TestCase
# Create your tests here.
# Import all test modules to ensure they are discovered by Django's test runner
from .tests.test_pricing import *
from .tests.test_pricing_edge_cases import *
from .tests.test_pricing_integration import *

View file

@ -0,0 +1,169 @@
# Pricing Model Tests Documentation
This directory contains comprehensive tests for the Django pricing models in the Servala project. The tests ensure that price calculations work correctly and prevent regressions when making changes to the pricing logic.
## Test Structure
### `test_pricing.py`
Core tests for all pricing models including:
- **ComputePlanTestCase**: Tests for compute plan pricing and retrieval
- **StoragePlanTestCase**: Tests for storage plan pricing
- **ProgressiveDiscountModelTestCase**: Tests for discount calculations with multiple tiers
- **VSHNAppCatPriceTestCase**: Tests for AppCat service pricing with discounts
- **VSHNAppCatAddonTestCase**: Tests for addon pricing (base fees and unit rates)
### `test_pricing_edge_cases.py`
Edge case and error condition tests including:
- Discount calculations with zero units or very large numbers
- Price calculations with inactive discount models
- Missing price data handling
- Decimal precision edge cases
- Unique constraint enforcement
- Addon ordering and filtering
### `test_pricing_integration.py`
Integration tests that verify models work together:
- Complete pricing setup across all models
- Multi-currency pricing scenarios
- Complex AppCat services with all features
- External price comparisons
- Real-world comprehensive pricing scenarios
### `test_utils.py`
Helper utilities and mixins for test setup:
- `PricingTestMixin`: Common setup methods for pricing tests
- Helper functions for expected price calculations
## Running the Tests
### Individual Test Classes
```bash
# Run specific test class
uv run --extra dev manage.py test hub.services.tests.test_pricing.ComputePlanTestCase --verbosity=2
# Run specific test method
uv run --extra dev manage.py test hub.services.tests.test_pricing.VSHNAppCatPriceTestCase.test_calculate_final_price_simple --verbosity=2
```
### All Pricing Tests
```bash
# Run all pricing tests
uv run --extra dev manage.py test hub.services.tests --verbosity=2
# Run with database reuse for faster execution
uv run --extra dev manage.py test hub.services.tests --verbosity=2 --keepdb
```
### Test Script
Use the provided test runner script:
```bash
./run_pricing_tests.sh
```
## Test Coverage
The test suite covers:
### Basic Model Functionality
- Model creation and string representations
- Field validation and constraints
- Relationship handling
### Price Calculations
- Simple price retrieval (`get_price` methods)
- Progressive discount calculations
- Final price calculations with base fees, unit rates, and addons
- Multi-currency support
### Edge Cases
- Zero or negative values
- Very large numbers
- Missing price data
- Inactive discount models
- Decimal precision issues
### Business Logic
- Mandatory vs. optional addons
- Discount tier spanning
- Service level pricing differences
- External price comparisons
### Integration Scenarios
- Complete service setups
- Real-world pricing scenarios
- Cross-model relationships
## Key Test Scenarios
### Progressive Discount Testing
The tests verify that progressive discounts work correctly:
```python
# Example: 0-9 units (0%), 10-49 units (10%), 50+ units (20%)
# For 60 units:
# - First 10 units: full price
# - Next 40 units: 90% of price (10% discount)
# - Next 10 units: 80% of price (20% discount)
```
### Addon Pricing Testing
Tests cover both addon types:
- **Base Fee Addons**: Fixed cost regardless of units
- **Unit Rate Addons**: Cost multiplied by number of units
### Multi-Currency Testing
Ensures pricing works across CHF, EUR, and USD currencies.
## Best Practices for Adding New Tests
1. **Use Descriptive Test Names**: Test method names should clearly describe what is being tested
2. **Test Both Success and Failure Cases**: Include tests for valid inputs and error conditions
3. **Use Decimal for Monetary Values**: Always use `Decimal` for precise monetary calculations
4. **Test Edge Cases**: Include tests for boundary conditions and unusual inputs
5. **Verify Relationships**: Test that model relationships work correctly
6. **Test Business Logic**: Focus on the actual business rules, not just CRUD operations
## Common Test Patterns
### Setting Up Test Data
```python
def setUp(self):
self.cloud_provider = CloudProvider.objects.create(
name="Test Provider",
slug="test-provider",
description="Test description",
website="https://test.com"
)
```
### Testing Price Calculations
```python
def test_price_calculation(self):
# Set up pricing data
# Call calculation method
result = price_config.calculate_final_price(currency, service_level, units)
# Verify expected result
self.assertEqual(result['total_price'], expected_amount)
```
### Testing Error Conditions
```python
def test_negative_units_error(self):
with self.assertRaises(ValueError):
price_config.calculate_final_price(currency, service_level, -1)
```
## Maintenance
- Run tests after any changes to pricing models or calculations
- Update tests when adding new pricing features
- Add regression tests when fixing bugs
- Keep test data realistic but minimal
- Document complex test scenarios in comments
## Dependencies
The tests require:
- Django test framework
- Test database (SQLite in-memory for speed)
- All pricing models and their dependencies
- Decimal library for precise calculations

View file

@ -0,0 +1 @@
# Tests for services app

View file

@ -0,0 +1,624 @@
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)

View file

@ -0,0 +1,325 @@
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)

View file

@ -0,0 +1,532 @@
from decimal import Decimal
from django.test import TestCase
from django.utils import timezone
from ..models.base import Currency, Term, Unit
from ..models.providers import CloudProvider
from ..models.services import Service, Category
from ..models.pricing import (
ComputePlan,
ComputePlanPrice,
ComputePlanGroup,
StoragePlan,
StoragePlanPrice,
ProgressiveDiscountModel,
DiscountTier,
VSHNAppCatPrice,
VSHNAppCatBaseFee,
VSHNAppCatUnitRate,
VSHNAppCatAddon,
VSHNAppCatAddonBaseFee,
VSHNAppCatAddonUnitRate,
ExternalPricePlans,
)
class PricingIntegrationTestCase(TestCase):
"""Integration tests for pricing models working together"""
def setUp(self):
"""Set up test data for integration tests"""
# Create cloud provider
self.cloud_provider = CloudProvider.objects.create(
name="VSHN Cloud",
slug="vshn-cloud",
description="Swiss cloud provider",
website="https://vshn.ch",
is_featured=True,
)
# Create service category
self.database_category = Category.objects.create(
name="Databases", slug="databases", description="Database services"
)
# Create database service
self.postgresql_service = Service.objects.create(
name="PostgreSQL",
slug="postgresql",
description="Managed PostgreSQL database service",
tagline="Reliable, scalable PostgreSQL",
features="High availability, automated backups, monitoring",
is_featured=True,
)
self.postgresql_service.categories.add(self.database_category)
# Create compute plan group
self.standard_group = ComputePlanGroup.objects.create(
name="Standard",
description="Standard compute plans",
node_label="standard",
order=1,
)
# Create multiple compute plans
self.small_plan = ComputePlan.objects.create(
name="Small",
vcpus=1.0,
ram=2.0,
cpu_mem_ratio=0.5,
cloud_provider=self.cloud_provider,
group=self.standard_group,
term=Term.MTH,
active=True,
)
self.medium_plan = ComputePlan.objects.create(
name="Medium",
vcpus=2.0,
ram=4.0,
cpu_mem_ratio=0.5,
cloud_provider=self.cloud_provider,
group=self.standard_group,
term=Term.MTH,
active=True,
)
self.large_plan = ComputePlan.objects.create(
name="Large",
vcpus=4.0,
ram=8.0,
cpu_mem_ratio=0.5,
cloud_provider=self.cloud_provider,
group=self.standard_group,
term=Term.MTH,
active=True,
)
# Create storage plan
self.ssd_storage = StoragePlan.objects.create(
name="SSD Storage",
cloud_provider=self.cloud_provider,
term=Term.MTH,
unit=Unit.GIB,
)
# Create progressive discount model for AppCat
self.ram_discount_model = ProgressiveDiscountModel.objects.create(
name="RAM Volume Discount",
description="Progressive discount for RAM usage",
active=True,
)
# Create discount tiers
DiscountTier.objects.create(
discount_model=self.ram_discount_model,
min_units=0,
max_units=8,
discount_percent=Decimal("0.00"), # 0-7 GiB: no discount
)
DiscountTier.objects.create(
discount_model=self.ram_discount_model,
min_units=8,
max_units=32,
discount_percent=Decimal("10.00"), # 8-31 GiB: 10% discount
)
DiscountTier.objects.create(
discount_model=self.ram_discount_model,
min_units=32,
max_units=None,
discount_percent=Decimal("20.00"), # 32+ GiB: 20% discount
)
def test_complete_pricing_setup(self):
"""Test complete pricing setup for all models"""
# Set up compute plan prices
ComputePlanPrice.objects.create(
compute_plan=self.small_plan, currency=Currency.CHF, amount=Decimal("50.00")
)
ComputePlanPrice.objects.create(
compute_plan=self.medium_plan,
currency=Currency.CHF,
amount=Decimal("100.00"),
)
ComputePlanPrice.objects.create(
compute_plan=self.large_plan,
currency=Currency.CHF,
amount=Decimal("200.00"),
)
# Set up storage pricing
StoragePlanPrice.objects.create(
storage_plan=self.ssd_storage,
currency=Currency.CHF,
amount=Decimal("0.20"), # 0.20 CHF per GiB
)
# Verify all prices are retrievable
self.assertEqual(self.small_plan.get_price(Currency.CHF), Decimal("50.00"))
self.assertEqual(self.medium_plan.get_price(Currency.CHF), Decimal("100.00"))
self.assertEqual(self.large_plan.get_price(Currency.CHF), Decimal("200.00"))
self.assertEqual(self.ssd_storage.get_price(Currency.CHF), Decimal("0.20"))
def test_multi_currency_pricing(self):
"""Test pricing in multiple currencies"""
# Set up prices in CHF, EUR, and USD
currencies_and_rates = [
(Currency.CHF, Decimal("100.00")),
(Currency.EUR, Decimal("95.00")),
(Currency.USD, Decimal("110.00")),
]
for currency, amount in currencies_and_rates:
ComputePlanPrice.objects.create(
compute_plan=self.medium_plan, currency=currency, amount=amount
)
# Verify all currencies are available
for currency, expected_amount in currencies_and_rates:
self.assertEqual(self.medium_plan.get_price(currency), expected_amount)
def test_appcat_service_with_complete_pricing(self):
"""Test complete AppCat service pricing with all features"""
# Create AppCat price configuration
appcat_price = VSHNAppCatPrice.objects.create(
service=self.postgresql_service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
discount_model=self.ram_discount_model,
ha_replica_min=1,
ha_replica_max=3,
public_display_enabled=True,
)
# Set up base fees for different currencies
base_fees = [
(Currency.CHF, Decimal("25.00")),
(Currency.EUR, Decimal("22.50")),
(Currency.USD, Decimal("27.50")),
]
for currency, amount in base_fees:
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=appcat_price, currency=currency, amount=amount
)
# Set up unit rates for different service levels and currencies
unit_rates = [
(Currency.CHF, VSHNAppCatPrice.ServiceLevel.BEST_EFFORT, Decimal("3.5000")),
(Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, Decimal("5.0000")),
(Currency.EUR, VSHNAppCatPrice.ServiceLevel.BEST_EFFORT, Decimal("3.2000")),
(Currency.EUR, VSHNAppCatPrice.ServiceLevel.GUARANTEED, Decimal("4.5000")),
(Currency.USD, VSHNAppCatPrice.ServiceLevel.BEST_EFFORT, Decimal("3.8000")),
(Currency.USD, VSHNAppCatPrice.ServiceLevel.GUARANTEED, Decimal("5.5000")),
]
for currency, service_level, amount in unit_rates:
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=appcat_price,
currency=currency,
service_level=service_level,
amount=amount,
)
# Create mandatory addon (backup)
backup_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=appcat_price,
name="Automated Backup",
description="Daily automated backups with 30-day retention",
commercial_description="Never lose your data with automated daily backups",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
mandatory=True,
order=1,
)
VSHNAppCatAddonBaseFee.objects.create(
addon=backup_addon, currency=Currency.CHF, amount=Decimal("15.00")
)
# Create optional addon (monitoring)
monitoring_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=appcat_price,
name="Advanced Monitoring",
description="Detailed monitoring with custom alerts",
commercial_description="Get insights into your database performance",
addon_type=VSHNAppCatAddon.AddonType.UNIT_RATE,
mandatory=False,
order=2,
)
VSHNAppCatAddonUnitRate.objects.create(
addon=monitoring_addon,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("0.5000"),
)
# Test price calculation scenarios
# Scenario 1: Small setup (4 GiB RAM, no discount)
result_small = appcat_price.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 4
)
# Base: 25 + (5 * 4) = 45
# Mandatory backup: 15
# Total: 60
self.assertEqual(result_small["total_price"], Decimal("60.00"))
self.assertEqual(result_small["addon_total"], Decimal("15.00"))
self.assertEqual(len(result_small["addon_breakdown"]), 1)
# Scenario 2: Medium setup (16 GiB RAM, partial discount)
result_medium = appcat_price.calculate_final_price(
Currency.CHF, VSHNAppCatPrice.ServiceLevel.GUARANTEED, 16
)
# First 8 GiB at full rate: 5 * 8 = 40
# Next 8 GiB at 90% (10% discount): 5 * 0.9 * 8 = 36
# Unit cost: 76
# Base: 25 + 76 = 101
# Mandatory backup: 15
# Total: 116
self.assertEqual(result_medium["total_price"], Decimal("116.00"))
# Scenario 3: Large setup with optional addon (40 GiB RAM, full discount tiers)
result_large = appcat_price.calculate_final_price(
Currency.CHF,
VSHNAppCatPrice.ServiceLevel.GUARANTEED,
40,
addon_ids=[monitoring_addon.id],
)
# First 8 GiB at full rate: 5 * 8 = 40
# Next 24 GiB at 90% (10% discount): 5 * 0.9 * 24 = 108
# Next 8 GiB at 80% (20% discount): 5 * 0.8 * 8 = 32
# Unit cost: 180
# Base: 25 + 180 = 205
# Mandatory backup: 15
# Optional monitoring: 0.5 * 40 = 20
# Total: 240
self.assertEqual(result_large["total_price"], Decimal("240.00"))
self.assertEqual(result_large["addon_total"], Decimal("35.00"))
self.assertEqual(len(result_large["addon_breakdown"]), 2)
def test_external_price_comparison_integration(self):
"""Test external price comparison with internal pricing"""
# Set up internal pricing
appcat_price = VSHNAppCatPrice.objects.create(
service=self.postgresql_service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
)
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=appcat_price,
currency=Currency.USD,
amount=Decimal("30.00"),
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=appcat_price,
currency=Currency.USD,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("4.0000"),
)
# Create external competitor pricing
aws_provider = CloudProvider.objects.create(
name="AWS",
slug="aws",
description="Amazon Web Services",
website="https://aws.amazon.com",
)
external_price = ExternalPricePlans.objects.create(
plan_name="RDS PostgreSQL db.t3.medium",
description="AWS RDS PostgreSQL instance",
source="https://aws.amazon.com/rds/postgresql/pricing/",
cloud_provider=aws_provider,
service=self.postgresql_service,
vshn_appcat_price=appcat_price,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
currency=Currency.USD,
term=Term.MTH,
amount=Decimal("62.56"), # Monthly cost for db.t3.medium
vcpus=2.0,
ram=4.0, # 4 GiB RAM
storage=20.0, # 20 GiB storage included
competitor_sla="99.95%",
replicas=1,
)
# Compare internal vs external pricing for equivalent setup
internal_result = appcat_price.calculate_final_price(
Currency.USD,
VSHNAppCatPrice.ServiceLevel.GUARANTEED,
4, # 4 GiB RAM to match external offering
)
# Internal: 30 + (4 * 4) = 46 USD
internal_price = internal_result["total_price"]
external_price_amount = external_price.amount
self.assertEqual(internal_price, Decimal("46.00"))
self.assertEqual(external_price_amount, Decimal("62.56"))
# Verify our pricing is competitive
self.assertLess(internal_price, external_price_amount)
def test_service_availability_with_pricing(self):
"""Test service availability based on pricing configuration"""
# Create service with pricing but not enabled for public display
redis_service = Service.objects.create(
name="Redis",
slug="redis",
description="In-memory data store",
features="High performance caching",
)
redis_price = VSHNAppCatPrice.objects.create(
service=redis_service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
public_display_enabled=False, # Private pricing
)
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=redis_price,
currency=Currency.CHF,
amount=Decimal("20.00"),
)
# Service should exist but not be publicly available for pricing
self.assertFalse(redis_price.public_display_enabled)
# Enable public display
redis_price.public_display_enabled = True
redis_price.save()
self.assertTrue(redis_price.public_display_enabled)
def test_pricing_model_relationships(self):
"""Test all pricing model relationships work correctly"""
# Verify cloud provider relationships
self.assertEqual(self.cloud_provider.compute_plans.count(), 3)
self.assertEqual(self.cloud_provider.storage_plans.count(), 1)
# Verify service relationships
self.assertTrue(hasattr(self.postgresql_service, "vshn_appcat_price"))
# Verify compute plan group relationships
self.assertEqual(self.standard_group.compute_plans.count(), 3)
# Create and verify discount model relationships
appcat_price = VSHNAppCatPrice.objects.create(
service=self.postgresql_service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
discount_model=self.ram_discount_model,
)
self.assertEqual(self.ram_discount_model.price_configs.count(), 1)
self.assertEqual(self.ram_discount_model.tiers.count(), 3)
# Test cascade deletions work properly
service_id = self.postgresql_service.id
appcat_price_id = appcat_price.id
# Delete service should cascade to appcat price
self.postgresql_service.delete()
with self.assertRaises(VSHNAppCatPrice.DoesNotExist):
VSHNAppCatPrice.objects.get(id=appcat_price_id)
def test_comprehensive_pricing_scenario(self):
"""Test a comprehensive real-world pricing scenario"""
# Company needs PostgreSQL with high availability
# Requirements: 16 GiB RAM, automated backups, monitoring, SSL
appcat_price = VSHNAppCatPrice.objects.create(
service=self.postgresql_service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
discount_model=self.ram_discount_model,
ha_replica_min=2,
ha_replica_max=3,
public_display_enabled=True,
)
# Set up pricing
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=appcat_price,
currency=Currency.CHF,
amount=Decimal("40.00"), # Base fee for managed service
)
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=appcat_price,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("6.0000"), # CHF per GiB RAM
)
# Create all required addons
backup_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=appcat_price,
name="Enterprise Backup",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
mandatory=True,
order=1,
)
VSHNAppCatAddonBaseFee.objects.create(
addon=backup_addon, currency=Currency.CHF, amount=Decimal("25.00")
)
monitoring_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=appcat_price,
name="Advanced Monitoring",
addon_type=VSHNAppCatAddon.AddonType.UNIT_RATE,
mandatory=False,
order=2,
)
VSHNAppCatAddonUnitRate.objects.create(
addon=monitoring_addon,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=Decimal("0.7500"),
)
ssl_addon = VSHNAppCatAddon.objects.create(
vshn_appcat_price_config=appcat_price,
name="SSL Certificate",
addon_type=VSHNAppCatAddon.AddonType.BASE_FEE,
mandatory=False,
order=3,
)
VSHNAppCatAddonBaseFee.objects.create(
addon=ssl_addon, currency=Currency.CHF, amount=Decimal("18.00")
)
# Calculate final price with all selected addons
result = appcat_price.calculate_final_price(
Currency.CHF,
VSHNAppCatPrice.ServiceLevel.GUARANTEED,
16, # 16 GiB RAM
addon_ids=[monitoring_addon.id, ssl_addon.id],
)
# Expected calculation:
# Base fee: 40.00
# RAM cost: First 8 at 6.00 = 48.00, Next 8 at 5.40 (10% discount) = 43.20
# RAM total: 91.20
# Mandatory backup: 25.00
# Optional monitoring: 0.75 * 16 = 12.00
# Optional SSL: 18.00
# Total: 40.00 + 91.20 + 25.00 + 12.00 + 18.00 = 186.20
self.assertEqual(result["total_price"], Decimal("186.20"))
self.assertEqual(result["addon_total"], Decimal("55.00"))
self.assertEqual(len(result["addon_breakdown"]), 3)
# Verify addon breakdown details
addon_names = [addon["name"] for addon in result["addon_breakdown"]]
self.assertIn("Enterprise Backup", addon_names)
self.assertIn("Advanced Monitoring", addon_names)
self.assertIn("SSL Certificate", addon_names)

View file

@ -0,0 +1,125 @@
"""
Test utilities and fixtures for pricing tests
"""
from decimal import Decimal
from django.test import TestCase
from ..models.base import Currency, Term, Unit
from ..models.providers import CloudProvider
from ..models.services import Service, Category
from ..models.pricing import (
ProgressiveDiscountModel,
DiscountTier,
VSHNAppCatPrice,
VSHNAppCatBaseFee,
VSHNAppCatUnitRate,
)
class PricingTestMixin:
"""Mixin providing common setup for pricing tests"""
def create_test_cloud_provider(self, name="Test Provider"):
"""Create a test cloud provider"""
return CloudProvider.objects.create(
name=name,
slug=name.lower().replace(" ", "-"),
description=f"{name} description",
website=f"https://{name.lower().replace(' ', '')}.com",
)
def create_test_service(self, name="Test Service"):
"""Create a test service"""
return Service.objects.create(
name=name,
slug=name.lower().replace(" ", "-"),
description=f"{name} description",
features=f"{name} features",
)
def create_test_discount_model(self, name="Test Discount", tiers=None):
"""Create a test discount model with optional tiers"""
discount_model = ProgressiveDiscountModel.objects.create(
name=name, description=f"{name} description", active=True
)
if tiers is None:
# Default tiers: 0-9 (0%), 10-49 (10%), 50+ (20%)
tiers = [
(0, 10, Decimal("0.00")),
(10, 50, Decimal("10.00")),
(50, None, Decimal("20.00")),
]
for min_units, max_units, discount_percent in tiers:
DiscountTier.objects.create(
discount_model=discount_model,
min_units=min_units,
max_units=max_units,
discount_percent=discount_percent,
)
return discount_model
def create_test_appcat_price(
self,
service,
discount_model=None,
base_fee=Decimal("50.00"),
unit_rate=Decimal("5.0000"),
):
"""Create a test AppCat price configuration"""
appcat_price = VSHNAppCatPrice.objects.create(
service=service,
variable_unit=VSHNAppCatPrice.VariableUnit.RAM,
term=Term.MTH,
discount_model=discount_model,
)
# Create base fee
VSHNAppCatBaseFee.objects.create(
vshn_appcat_price_config=appcat_price,
currency=Currency.CHF,
amount=base_fee,
)
# Create unit rate
VSHNAppCatUnitRate.objects.create(
vshn_appcat_price_config=appcat_price,
currency=Currency.CHF,
service_level=VSHNAppCatPrice.ServiceLevel.GUARANTEED,
amount=unit_rate,
)
return appcat_price
def calculate_expected_discounted_price(base_rate, units, discount_tiers):
"""Helper function to calculate expected discounted price manually"""
final_price = Decimal("0")
remaining_units = units
# Sort tiers by min_units
sorted_tiers = sorted(discount_tiers, key=lambda x: x[0])
for min_units, max_units, discount_percent in sorted_tiers:
if remaining_units <= 0:
break
# Skip if we haven't reached this tier yet
if units < min_units:
continue
# Calculate units in this tier
tier_max = max_units if max_units else float("inf")
units_start = max(0, units - remaining_units)
units_end = min(units, tier_max if max_units else units)
tier_units = max(0, units_end - max(units_start, min_units - 1))
if tier_units > 0:
discounted_rate = base_rate * (1 - discount_percent / 100)
final_price += discounted_rate * tier_units
remaining_units -= tier_units
return final_price