website/hub/services/views/pricelist.py

234 lines
9.8 KiB
Python
Raw Normal View History

2025-05-22 16:34:15 +02:00
from django.shortcuts import render
2025-05-23 15:56:11 +02:00
import re
from collections import defaultdict
from hub.services.models import ComputePlan, VSHNAppCatPrice
def natural_sort_key(name):
2025-05-23 17:09:02 +02:00
"""Extract numeric part from compute plan name for natural sorting"""
2025-05-23 15:56:11 +02:00
match = re.search(r"compute-std-(\d+)", name)
return int(match.group(1)) if match else 0
2025-05-22 16:34:15 +02:00
2025-05-23 08:49:08 +02:00
def pricelist(request):
2025-05-23 17:09:02 +02:00
"""Generate comprehensive price list grouped by compute plan groups and service levels"""
# Check if discount details should be shown
show_discount_details = request.GET.get("discount_details", "").lower() == "true"
2025-05-23 17:09:02 +02:00
# Fetch all active compute plans with related data
2025-05-22 16:34:15 +02:00
compute_plans = (
2025-05-23 15:56:11 +02:00
ComputePlan.objects.filter(active=True)
2025-05-23 17:09:02 +02:00
.select_related("cloud_provider", "group")
2025-05-22 16:34:15 +02:00
.prefetch_related("prices")
2025-05-23 17:09:02 +02:00
.order_by("group__order", "group__name", "cloud_provider__name")
2025-05-23 15:56:11 +02:00
)
2025-05-23 17:09:02 +02:00
# Apply natural sorting for compute plan names
2025-05-23 15:56:11 +02:00
compute_plans = sorted(
2025-05-23 17:09:02 +02:00
compute_plans,
key=lambda x: (
x.group.order if x.group else 999, # No group plans at the end
x.group.name if x.group else "ZZZ",
x.cloud_provider.name,
natural_sort_key(x.name),
),
2025-05-22 16:34:15 +02:00
)
2025-05-23 15:56:11 +02:00
2025-05-23 17:09:02 +02:00
# Fetch all appcat price configurations
2025-05-22 16:34:15 +02:00
appcat_prices = (
VSHNAppCatPrice.objects.all()
2025-05-23 08:49:08 +02:00
.select_related("service", "discount_model")
2025-05-23 15:56:11 +02:00
.prefetch_related("base_fees", "unit_rates", "discount_model__tiers")
.order_by("service__name")
2025-05-22 16:34:15 +02:00
)
2025-05-23 17:09:02 +02:00
pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list))
2025-05-23 15:56:11 +02:00
processed_combinations = set()
2025-05-22 16:34:15 +02:00
2025-05-23 17:09:02 +02:00
# Generate pricing combinations for each compute plan and service
2025-05-22 16:34:15 +02:00
for plan in compute_plans:
2025-05-23 15:56:11 +02:00
plan_currencies = set(plan.prices.values_list("currency", flat=True))
2025-05-22 16:34:15 +02:00
2025-05-23 15:56:11 +02:00
for appcat_price in appcat_prices:
2025-05-23 17:09:02 +02:00
# Determine units based on variable unit type
2025-05-23 15:56:11 +02:00
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
2025-05-22 16:34:15 +02:00
units = int(plan.ram)
2025-05-23 15:56:11 +02:00
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
2025-05-22 16:34:15 +02:00
units = int(plan.vcpus)
else:
2025-05-23 15:56:11 +02:00
continue
base_fee_currencies = set(
appcat_price.base_fees.values_list("currency", flat=True)
)
2025-05-22 16:34:15 +02:00
2025-05-23 15:56:11 +02:00
service_levels = appcat_price.unit_rates.values_list(
"service_level", flat=True
2025-05-22 16:34:15 +02:00
).distinct()
for service_level in service_levels:
2025-05-23 15:56:11 +02:00
unit_rate_currencies = set(
appcat_price.unit_rates.filter(
service_level=service_level
).values_list("currency", flat=True)
)
2025-05-23 17:09:02 +02:00
# Find currencies that exist across all pricing components
2025-05-23 15:56:11 +02:00
matching_currencies = plan_currencies.intersection(
base_fee_currencies
).intersection(unit_rate_currencies)
if not matching_currencies:
continue
for currency in matching_currencies:
combination_key = (
plan.cloud_provider.name,
plan.name,
appcat_price.service.name,
service_level,
currency,
2025-05-22 16:34:15 +02:00
)
2025-05-23 17:09:02 +02:00
# Skip if combination already processed
2025-05-23 15:56:11 +02:00
if combination_key in processed_combinations:
continue
processed_combinations.add(combination_key)
2025-05-23 17:09:02 +02:00
# Get pricing components
2025-05-23 15:56:11 +02:00
compute_plan_price = plan.get_price(currency)
base_fee = appcat_price.get_base_fee(currency)
unit_rate = appcat_price.get_unit_rate(currency, service_level)
2025-05-23 17:09:02 +02:00
# Skip if any pricing component is missing
if any(
price is None
for price in [compute_plan_price, base_fee, unit_rate]
):
2025-05-23 15:56:11 +02:00
continue
2025-05-23 17:09:02 +02:00
# Calculate replica enforcement based on service level
2025-05-23 15:56:11 +02:00
if service_level == VSHNAppCatPrice.ServiceLevel.GUARANTEED:
replica_enforce = appcat_price.ha_replica_min
else:
replica_enforce = 1
total_units = units * replica_enforce
2025-05-23 16:37:03 +02:00
standard_sla_price = base_fee + (total_units * unit_rate)
2025-05-23 15:56:11 +02:00
2025-05-23 17:09:02 +02:00
# Apply discount if available
2025-05-23 16:37:03 +02:00
discount_breakdown = None
2025-05-23 15:56:11 +02:00
if (
appcat_price.discount_model
and appcat_price.discount_model.active
):
discounted_price = (
appcat_price.discount_model.calculate_discount(
unit_rate, total_units
)
2025-05-22 16:34:15 +02:00
)
2025-05-23 15:56:11 +02:00
sla_price = base_fee + discounted_price
2025-05-23 16:37:03 +02:00
discount_savings = standard_sla_price - sla_price
discount_percentage = (
(discount_savings / standard_sla_price) * 100
if standard_sla_price > 0
else 0
)
discount_breakdown = (
appcat_price.discount_model.get_discount_breakdown(
unit_rate, total_units
)
)
2025-05-23 15:56:11 +02:00
else:
2025-05-23 16:37:03 +02:00
sla_price = standard_sla_price
discounted_price = total_units * unit_rate
discount_savings = 0
discount_percentage = 0
2025-05-23 15:56:11 +02:00
final_price = compute_plan_price + sla_price
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
service_level
]
2025-05-23 17:09:02 +02:00
group_name = plan.group.name if plan.group else "No Group"
# Add pricing data to the grouped structure
pricing_data_by_group_and_service_level[group_name][
service_level_display
].append(
2025-05-23 15:56:11 +02:00
{
"cloud_provider": plan.cloud_provider.name,
"service": appcat_price.service.name,
"compute_plan": plan.name,
2025-05-23 17:09:02 +02:00
"compute_plan_group": group_name,
"compute_plan_group_description": (
plan.group.description if plan.group else ""
),
"compute_plan_group_node_label": (
plan.group.node_label if plan.group else ""
),
2025-05-23 15:56:11 +02:00
"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,
2025-05-23 16:37:03 +02:00
"total_units": total_units,
2025-05-23 15:56:11 +02:00
"service_level": service_level_display,
"sla_base": base_fee,
"sla_per_unit": unit_rate,
"sla_price": sla_price,
2025-05-23 16:37:03 +02:00
"standard_sla_price": standard_sla_price,
"discounted_sla_price": (
base_fee + discounted_price
if appcat_price.discount_model
and appcat_price.discount_model.active
else None
),
"discount_savings": discount_savings,
"discount_percentage": discount_percentage,
"discount_breakdown": discount_breakdown,
2025-05-23 15:56:11 +02:00
"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
),
}
)
2025-05-22 16:34:15 +02:00
2025-05-23 17:09:02 +02:00
# Order groups correctly, placing "No Group" last
ordered_groups_intermediate = {}
all_group_names = list(pricing_data_by_group_and_service_level.keys())
if "No Group" in all_group_names:
all_group_names.remove("No Group")
all_group_names.append("No Group")
for group_name_key in all_group_names:
ordered_groups_intermediate[group_name_key] = (
pricing_data_by_group_and_service_level[group_name_key]
)
# Convert defaultdicts to regular dicts for the template
final_context_data = {}
for group_key, service_levels_dict in ordered_groups_intermediate.items():
final_context_data[group_key] = {
sl_key: list(plans_list)
for sl_key, plans_list in service_levels_dict.items()
}
context = {
"pricing_data_by_group_and_service_level": final_context_data,
"show_discount_details": show_discount_details,
}
2025-05-22 16:34:15 +02:00
return render(request, "services/pricelist.html", context)