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