website/hub/services/views/offerings.py

252 lines
8.8 KiB
Python

from django.shortcuts import render, get_object_or_404
from django.db.models import Q
from hub.services.models import (
ServiceOffering,
CloudProvider,
Category,
Service,
ComputePlan,
VSHNAppCatPrice,
)
import re
from collections import defaultdict
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 offering_list(request):
offerings = (
ServiceOffering.objects.filter(disable_listing=False)
.order_by("service")
.select_related("service", "cloud_provider")
.prefetch_related(
"service__categories",
"plans",
)
)
cloud_providers = CloudProvider.objects.all()
categories = Category.objects.filter(parent=None).prefetch_related("children")
services = Service.objects.all().order_by("name")
# Handle cloud provider filter
if request.GET.get("cloud_provider"):
provider_id = request.GET.get("cloud_provider")
offerings = offerings.filter(cloud_provider_id=provider_id)
# Handle category filter
if request.GET.get("category"):
category_id = request.GET.get("category")
category = get_object_or_404(Category, id=category_id)
subcategories = Category.objects.filter(parent=category)
offerings = offerings.filter(
Q(service__categories=category) | Q(service__categories__in=subcategories)
).distinct()
# Handle service filter
if request.GET.get("service"):
service_id = request.GET.get("service")
offerings = offerings.filter(service_id=service_id)
# Handle search
if request.GET.get("search"):
query = request.GET.get("search")
offerings = offerings.filter(
Q(service__name__icontains=query)
| Q(description__icontains=query)
| Q(cloud_provider__name__icontains=query)
).distinct()
context = {
"offerings": offerings,
"cloud_providers": cloud_providers,
"categories": categories,
"services": services,
}
return render(request, "services/offering_list.html", context)
def offering_detail(request, provider_slug, service_slug):
offering = get_object_or_404(
ServiceOffering.objects.select_related(
"service", "cloud_provider"
).prefetch_related("plans"),
cloud_provider__slug=provider_slug,
service__slug=service_slug,
)
pricing_data_by_group_and_service_level = None
# Generate pricing data for VSHN offerings
if offering.msp == "VS":
pricing_data_by_group_and_service_level = generate_pricing_data(offering)
context = {
"offering": offering,
"pricing_data_by_group_and_service_level": pricing_data_by_group_and_service_level,
}
return render(request, "services/offering_detail.html", context)
def generate_pricing_data(offering):
"""Generate pricing data for a specific offering and cloud provider"""
# Fetch compute plans for this cloud provider
compute_plans = (
ComputePlan.objects.filter(active=True, cloud_provider=offering.cloud_provider)
.select_related("cloud_provider", "group")
.prefetch_related("prices")
.order_by("group__order", "group__name")
)
# Apply natural sorting for compute plan names
compute_plans = sorted(
compute_plans,
key=lambda x: (
x.group.order if x.group else 999,
x.group.name if x.group else "ZZZ",
natural_sort_key(x.name),
),
)
# Fetch pricing for this specific service
try:
appcat_price = (
VSHNAppCatPrice.objects.select_related("service", "discount_model")
.prefetch_related("base_fees", "unit_rates", "discount_model__tiers")
.get(service=offering.service)
)
except VSHNAppCatPrice.DoesNotExist:
return None
pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list))
processed_combinations = set()
# Generate pricing combinations for each compute plan
for plan in compute_plans:
plan_currencies = set(plan.prices.values_list("currency", flat=True))
# Determine units based on variable unit type
if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM:
units = int(plan.ram)
elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU:
units = int(plan.vcpus)
else:
continue
base_fee_currencies = set(
appcat_price.base_fees.values_list("currency", flat=True)
)
service_levels = appcat_price.unit_rates.values_list(
"service_level", flat=True
).distinct()
for service_level in service_levels:
unit_rate_currencies = set(
appcat_price.unit_rates.filter(service_level=service_level).values_list(
"currency", flat=True
)
)
# Find currencies that exist across all pricing components
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.name,
service_level,
currency,
)
# Skip if combination already processed
if combination_key in processed_combinations:
continue
processed_combinations.add(combination_key)
# Get pricing components
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)
# Skip if any pricing component is missing
if any(
price is None for price in [compute_plan_price, base_fee, unit_rate]
):
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
standard_sla_price = base_fee + (total_units * unit_rate)
# Apply discount if available
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:
sla_price = standard_sla_price
final_price = compute_plan_price + sla_price
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
service_level
]
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(
{
"compute_plan": plan.name,
"compute_plan_group": group_name,
"compute_plan_group_description": (
plan.group.description if plan.group else ""
),
"vcpus": plan.vcpus,
"ram": plan.ram,
"currency": currency,
"compute_plan_price": compute_plan_price,
"sla_price": sla_price,
"final_price": final_price,
}
)
# Order groups correctly, placing "No Group" last
ordered_groups = {}
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[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.items():
final_context_data[group_key] = {
sl_key: list(plans_list)
for sl_key, plans_list in service_levels_dict.items()
}
return final_context_data