diff --git a/hub/services/models.py b/hub/services/models.py
index 0905f4f..89831d3 100644
--- a/hub/services/models.py
+++ b/hub/services/models.py
@@ -483,7 +483,7 @@ class StoragePlan(models.Model):
def get_price(self, currency_code: str):
try:
return self.prices.get(currency=currency_code).amount
- except ComputePlanPrice.DoesNotExist:
+ except StoragePlanPrice.DoesNotExist:
return None
diff --git a/hub/services/templates/services/pricelist.html b/hub/services/templates/services/pricelist.html
index 12ed6c0..4113a1e 100644
--- a/hub/services/templates/services/pricelist.html
+++ b/hub/services/templates/services/pricelist.html
@@ -1,94 +1,81 @@
{% extends 'base.html' %}
-{% block content %}
-
Compute Plan Price Comparison
+{% block title %}Complete Price List{% endblock %}
-{% for plan_data in plans_data %}
-
-
{% endblock %}
\ No newline at end of file
diff --git a/hub/services/views/pricelist.py b/hub/services/views/pricelist.py
index 227fb75..3ba2474 100644
--- a/hub/services/views/pricelist.py
+++ b/hub/services/views/pricelist.py
@@ -1,86 +1,174 @@
from django.shortcuts import render
-from hub.services.models import ComputePlan, VSHNAppCatPrice, VSHNAppCatUnitRate
+import re
+from collections import defaultdict
+from hub.services.models import ComputePlan, VSHNAppCatPrice
+
+
+def natural_sort_key(name):
+ """Extract numeric part from compute plan name for natural sorting"""
+ match = re.search(r"compute-std-(\d+)", name)
+ return int(match.group(1)) if match else 0
def pricelist(request):
- # Get all compute plans and app catalog prices
+ # Get all active compute plans with their pricing data
compute_plans = (
- ComputePlan.objects.all()
+ ComputePlan.objects.filter(active=True)
.select_related("cloud_provider")
.prefetch_related("prices")
+ .order_by("cloud_provider__name")
)
+
+ # Sort compute plans naturally by provider and plan number
+ compute_plans = sorted(
+ compute_plans, key=lambda x: (x.cloud_provider.name, natural_sort_key(x.name))
+ )
+
+ # Get all VSHNAppCat pricing configurations
appcat_prices = (
VSHNAppCatPrice.objects.all()
.select_related("service", "discount_model")
- .prefetch_related("base_fees", "unit_rates")
+ .prefetch_related("base_fees", "unit_rates", "discount_model__tiers")
+ .order_by("service__name")
)
- plans_data = []
+ pricing_data_by_service_level = defaultdict(list)
+
+ # Track processed combinations to avoid duplicates
+ processed_combinations = set()
for plan in compute_plans:
- plan_data = {"plan": plan, "calculated_prices": []}
+ # Get all currencies available for this compute plan
+ plan_currencies = set(plan.prices.values_list("currency", flat=True))
- for price_config in appcat_prices:
- # Get all service levels for this price config
- service_levels = (
- VSHNAppCatUnitRate.objects.filter(vshn_appcat_price_config=price_config)
- .values_list("service_level", flat=True)
- .distinct()
- )
-
- # Determine number of units based on variable_unit
- if price_config.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
+ for appcat_price in appcat_prices:
+ # Determine units based on variable unit type (RAM or CPU)
+ if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
units = int(plan.ram)
- elif price_config.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
+ elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
units = int(plan.vcpus)
else:
- continue # Skip other unit type as we don't know yet how to handle them
+ continue
- # Get all currencies used in base fees
- currencies = price_config.base_fees.values_list(
- "currency", flat=True
+ # Get currencies available for base fees and unit rates
+ base_fee_currencies = set(
+ appcat_price.base_fees.values_list("currency", flat=True)
+ )
+
+ # Get all distinct service levels for this pricing config
+ service_levels = appcat_price.unit_rates.values_list(
+ "service_level", flat=True
).distinct()
- # Calculate prices for all combinations
for service_level in service_levels:
- for currency in currencies:
- final_price = price_config.calculate_final_price(
- currency_code=currency,
- service_level=service_level,
- number_of_units=units,
+ # Get currencies available for this specific service level
+ unit_rate_currencies = set(
+ appcat_price.unit_rates.filter(
+ service_level=service_level
+ ).values_list("currency", flat=True)
+ )
+
+ # Find currencies that exist in ALL three places: plan, base fee, and unit rate
+ matching_currencies = plan_currencies.intersection(
+ base_fee_currencies
+ ).intersection(unit_rate_currencies)
+
+ # Skip if no common currencies found
+ if not matching_currencies:
+ continue
+
+ # Process each matching currency
+ for currency in matching_currencies:
+ # Create unique combination key to prevent duplicates
+ combination_key = (
+ plan.cloud_provider.name,
+ plan.name,
+ appcat_price.service.name,
+ service_level,
+ currency,
)
- if final_price is not None:
- service_level_display = dict(
- VSHNAppCatPrice.ServiceLevel.choices
- )[service_level]
+ # Skip if this combination was already processed
+ if combination_key in processed_combinations:
+ continue
- # Include discount model information
- discount_info = None
- if (
- price_config.discount_model
- and price_config.discount_model.active
- ):
- discount_info = {
- "name": price_config.discount_model.name,
- "description": price_config.discount_model.description,
- }
+ processed_combinations.add(combination_key)
- plan_data["calculated_prices"].append(
- {
- "service": price_config.service.name,
- "variable_unit": price_config.get_variable_unit_display(),
- "service_level": service_level_display,
- "units": units,
- "currency": currency,
- "price": final_price,
- "plan_term": plan.get_term_display(),
- "service_term": price_config.get_term_display(),
- "discount_model": discount_info,
- }
+ # Get pricing data for this currency - skip if any is missing
+ compute_plan_price = plan.get_price(currency)
+ if compute_plan_price is None:
+ continue
+
+ base_fee = appcat_price.get_base_fee(currency)
+ if base_fee is None:
+ continue
+
+ unit_rate = appcat_price.get_unit_rate(currency, service_level)
+ if unit_rate is None:
+ continue
+
+ # Calculate replica enforcement based on service level
+ if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
+ replica_enforce = appcat_price.ha_replica_min
+ else:
+ replica_enforce = 1
+
+ total_units = units * replica_enforce
+
+ # Apply discount model if available and active
+ if (
+ appcat_price.discount_model
+ and appcat_price.discount_model.active
+ ):
+ discounted_price = (
+ appcat_price.discount_model.calculate_discount(
+ unit_rate, total_units
+ )
)
+ sla_price = base_fee + discounted_price
+ else:
+ # Standard pricing without discount
+ sla_price = base_fee + (total_units * unit_rate)
- plans_data.append(plan_data)
+ # Calculate final price (compute + SLA)
+ final_price = compute_plan_price + sla_price
- context = {"plans_data": plans_data}
+ # Get human-readable service level name
+ service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
+ service_level
+ ]
+
+ # Add row to the appropriate service level group
+ pricing_data_by_service_level[service_level_display].append(
+ {
+ "cloud_provider": plan.cloud_provider.name,
+ "service": appcat_price.service.name,
+ "compute_plan": plan.name,
+ "vcpus": plan.vcpus,
+ "ram": plan.ram,
+ "cpu_mem_ratio": plan.cpu_mem_ratio,
+ "term": plan.get_term_display(),
+ "currency": currency,
+ "compute_plan_price": compute_plan_price,
+ "variable_unit": appcat_price.get_variable_unit_display(),
+ "units": units,
+ "replica_enforce": replica_enforce,
+ "service_level": service_level_display,
+ "sla_base": base_fee,
+ "sla_per_unit": unit_rate,
+ "sla_price": sla_price,
+ "final_price": final_price,
+ "discount_model": (
+ appcat_price.discount_model.name
+ if appcat_price.discount_model
+ else None
+ ),
+ "has_discount": bool(
+ appcat_price.discount_model
+ and appcat_price.discount_model.active
+ ),
+ }
+ )
+
+ context = {"pricing_data_by_service_level": dict(pricing_data_by_service_level)}
return render(request, "services/pricelist.html", context)