import re from django.shortcuts import render from collections import defaultdict from hub.services.models.pricing import ( ComputePlan, StoragePlan, ExternalPricePlans, VSHNAppCatPrice, ) from django.contrib.admin.views.decorators import staff_member_required from django.db import models def natural_sort_key(obj): """Extract numeric parts for natural sorting (works for any plan name)""" name = obj.name if hasattr(obj, "name") else str(obj) parts = re.split(r"(\d+)", name) return [int(part) if part.isdigit() else part for part in parts] def get_external_price_comparisons(plan, appcat_price, currency, service_level): """Get external price comparisons for a specific compute plan and service""" try: # Filter by service level if external price has one set, ignore currency for comparison external_prices = ExternalPricePlans.objects.filter( compare_to=plan, service=appcat_price.service ).select_related("cloud_provider") # Filter by service level if the external price has it configured if service_level: external_prices = external_prices.filter( models.Q(service_level=service_level) | models.Q(service_level__isnull=True) ) return external_prices except Exception: return [] def get_internal_cloud_provider_comparisons( plan, appcat_price, currency, service_level ): """Get internal comparisons with other cloud provider plans from the database""" try: # Get similar compute plans from other cloud providers with same specs similar_plans = ( ComputePlan.objects.filter( active=True, vcpus=plan.vcpus, ram=plan.ram, ) .exclude(cloud_provider=plan.cloud_provider) # Exclude same cloud provider .select_related("cloud_provider") .prefetch_related("prices") ) internal_comparisons = [] for similar_plan in similar_plans: # Get pricing components for comparison plan compare_plan_price = similar_plan.get_price(currency) compare_base_fee = appcat_price.get_base_fee(currency, service_level) compare_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 [compare_plan_price, compare_base_fee, compare_unit_rate] ): continue # Calculate units based on variable unit type if appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.RAM: units = int(similar_plan.ram) elif appcat_price.variable_unit == VSHNAppCatPrice.VariableUnit.CPU: units = int(similar_plan.vcpus) else: 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 # Calculate final price using the same logic as the main plan price_calculation = appcat_price.calculate_final_price( currency_code=currency, service_level=service_level, number_of_units=total_units, addon_ids=None, # Include only mandatory addons ) if price_calculation is None: continue service_price_with_addons = price_calculation["total_price"] compare_final_price = compare_plan_price + service_price_with_addons internal_comparisons.append( { "plan_name": similar_plan.name, "provider": similar_plan.cloud_provider.name, "compute_plan_price": compare_plan_price, "service_price": service_price_with_addons, "final_price": compare_final_price, "currency": currency, "vcpus": similar_plan.vcpus, "ram": similar_plan.ram, "group_name": ( similar_plan.group.name if similar_plan.group else "No Group" ), "is_internal": True, # Flag to distinguish from external comparisons } ) return internal_comparisons except Exception: return [] @staff_member_required def pricelist(request): """Generate comprehensive price list grouped by compute plan groups and service levels (optimized)""" # Get filter parameters from request show_discount_details = request.GET.get("discount_details", "").lower() == "true" show_addon_details = request.GET.get("addon_details", "").lower() == "true" show_price_comparison = request.GET.get("price_comparison", "").lower() == "true" filter_cloud_provider = request.GET.get("cloud_provider", "") filter_service = request.GET.get("service", "") filter_compute_plan_group = request.GET.get("compute_plan_group", "") filter_service_level = request.GET.get("service_level", "") # Get filter options for dropdowns first (needed for initial page load) all_cloud_providers = ( ComputePlan.objects.all() .values_list("cloud_provider__name", flat=True) .distinct() .order_by("cloud_provider__name") ) all_services = ( VSHNAppCatPrice.objects.values_list("service__name", flat=True) .distinct() .order_by("service__name") ) all_compute_plan_groups = list( ComputePlan.objects.filter(group__isnull=False) .values_list("group__name", flat=True) .distinct() .order_by("group__name") ) all_compute_plan_groups.append("No Group") # Add option for plans without groups all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices] # Only process pricing data if both cloud provider and service are selected if not filter_cloud_provider or not filter_service: context = { "pricing_data_by_group_and_service_level": {}, "show_discount_details": show_discount_details, "show_addon_details": show_addon_details, "show_price_comparison": show_price_comparison, "filter_cloud_provider": filter_cloud_provider, "filter_service": filter_service, "filter_compute_plan_group": filter_compute_plan_group, "filter_service_level": filter_service_level, "all_cloud_providers": all_cloud_providers, "all_services": all_services, "all_compute_plan_groups": all_compute_plan_groups, "all_service_levels": all_service_levels, "show_empty_state": True, # Flag to show empty state message } return render(request, "services/pricelist.html", context) # Fetch all compute plans (active and inactive) with related data compute_plans_qs = ComputePlan.objects.all() if filter_cloud_provider: compute_plans_qs = compute_plans_qs.filter( cloud_provider__name=filter_cloud_provider ) if filter_compute_plan_group: if filter_compute_plan_group == "No Group": compute_plans_qs = compute_plans_qs.filter(group__isnull=True) else: compute_plans_qs = compute_plans_qs.filter( group__name=filter_compute_plan_group ) compute_plans = list( compute_plans_qs.select_related("cloud_provider", "group") .prefetch_related("prices") .order_by("group__order", "group__name", "cloud_provider__name", "name") ) # Restore natural sorting of compute plan names compute_plans = sorted( compute_plans, key=lambda p: ( p.group.order if p.group else 999, p.group.name if p.group else "ZZZ", natural_sort_key(p), ), ) # Fetch all appcat price configurations (prefetch addons) appcat_prices_qs = ( VSHNAppCatPrice.objects.all() .select_related("service", "discount_model") .prefetch_related("base_fees", "unit_rates", "discount_model__tiers", "addons") .order_by("service__name") ) if filter_service: appcat_prices_qs = appcat_prices_qs.filter(service__name=filter_service) appcat_prices = list(appcat_prices_qs) # Prefetch all storage plans for all cloud providers and build a lookup all_storage_plans = StoragePlan.objects.all().prefetch_related("prices") storage_plans_by_provider = defaultdict(list) for sp in all_storage_plans: storage_plans_by_provider[sp.cloud_provider_id].append(sp) pricing_data_by_group_and_service_level = defaultdict(lambda: defaultdict(list)) processed_combinations = set() # Generate pricing combinations for each compute plan and service for plan in compute_plans: plan_currencies = set(plan.prices.values_list("currency", flat=True)) for appcat_price in appcat_prices: # 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() # Apply service level filter if filter_service_level: service_levels = [ sl for sl in service_levels if dict(VSHNAppCatPrice.ServiceLevel.choices)[sl] == filter_service_level ] 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.cloud_provider.name, plan.name, appcat_price.service.name, service_level, currency, ) 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, service_level) unit_rate = appcat_price.get_unit_rate(currency, service_level) 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 discount_breakdown = None 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 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 ) ) else: sla_price = standard_sla_price discounted_price = total_units * unit_rate discount_savings = 0 discount_percentage = 0 # Calculate final price using the model method to ensure consistency price_calculation = appcat_price.calculate_final_price( currency_code=currency, service_level=service_level, number_of_units=total_units, addon_ids=None, # This will include only mandatory addons ) if price_calculation is None: continue # Calculate base service price (without addons) for display purposes base_sla_price = base_fee + (total_units * unit_rate) # Extract addon information from the calculation (use prefetched addons) mandatory_addons = [] optional_addons = [] all_addons = [a for a in appcat_price.addons.all() if a.active] for addon in all_addons: addon_price = None if addon.addon_type == "BF": # Base Fee addon_price = addon.get_price(currency, service_level) elif addon.addon_type == "UR": # Unit Rate addon_price_per_unit = addon.get_price( currency, service_level ) if addon_price_per_unit: addon_price = addon_price_per_unit * total_units addon_info = { "id": addon.id, "name": addon.name, "description": addon.description, "commercial_description": addon.commercial_description, "addon_type": addon.get_addon_type_display(), "price": addon_price, } if addon.mandatory: mandatory_addons.append(addon_info) else: optional_addons.append(addon_info) service_price_with_addons = price_calculation["total_price"] final_price = compute_plan_price + service_price_with_addons service_level_display = dict( VSHNAppCatPrice.ServiceLevel.choices ).get(service_level, service_level) # Get external/internal price comparisons if enabled (unchanged, but could be optimized further) external_comparisons = [] internal_comparisons = [] if show_price_comparison: external_prices = get_external_price_comparisons( plan, appcat_price, currency, service_level ) for ext_price in external_prices: difference = ext_price.amount - final_price ratio = ( ext_price.amount / final_price if final_price > 0 else 0 ) external_comparisons.append( { "plan_name": ext_price.plan_name, "provider": ext_price.cloud_provider.name, "description": ext_price.description, "amount": ext_price.amount, "currency": ext_price.currency, "vcpus": ext_price.vcpus, "ram": ext_price.ram, "storage": ext_price.storage, "replicas": ext_price.replicas, "difference": difference, "ratio": ratio, "source": ext_price.source, "date_retrieved": ext_price.date_retrieved, "is_internal": False, } ) internal_price_comparisons = ( get_internal_cloud_provider_comparisons( plan, appcat_price, currency, service_level ) ) for int_price in internal_price_comparisons: difference = int_price["final_price"] - final_price ratio = ( int_price["final_price"] / final_price if final_price > 0 else 0 ) internal_comparisons.append( { "plan_name": int_price["plan_name"], "provider": int_price["provider"], "description": f"Same specs with {int_price['provider']}", "amount": int_price["final_price"], "currency": int_price["currency"], "vcpus": int_price["vcpus"], "ram": int_price["ram"], "group_name": int_price["group_name"], "compute_plan_price": int_price[ "compute_plan_price" ], "service_price": int_price["service_price"], "difference": difference, "ratio": ratio, "is_internal": True, } ) group_name = plan.group.name if plan.group else "No Group" # Use prefetched storage plans storage_plans = storage_plans_by_provider.get( plan.cloud_provider_id, [] ) pricing_data_by_group_and_service_level[group_name][ service_level_display ].append( { "cloud_provider": plan.cloud_provider.name, "service": appcat_price.service.name, "compute_plan": plan.name, "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 "" ), "storage_plans": storage_plans, "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, "total_units": total_units, "service_level": service_level_display, "sla_base": base_fee, "sla_per_unit": unit_rate, "sla_price": service_price_with_addons, "standard_sla_price": base_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, "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 ), "external_comparisons": external_comparisons, "internal_comparisons": internal_comparisons, "mandatory_addons": mandatory_addons, "optional_addons": optional_addons, "is_active": plan.active, } ) # 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() } # Get filter options for dropdowns (include all providers/groups from all plans, not just active) all_cloud_providers = ( ComputePlan.objects.all() .values_list("cloud_provider__name", flat=True) .distinct() .order_by("cloud_provider__name") ) all_services = ( VSHNAppCatPrice.objects.values_list("service__name", flat=True) .distinct() .order_by("service__name") ) all_compute_plan_groups = list( ComputePlan.objects.filter(group__isnull=False) .values_list("group__name", flat=True) .distinct() .order_by("group__name") ) all_compute_plan_groups.append("No Group") # Add option for plans without groups all_service_levels = [choice[1] for choice in VSHNAppCatPrice.ServiceLevel.choices] context = { "pricing_data_by_group_and_service_level": final_context_data, "show_discount_details": show_discount_details, "show_addon_details": show_addon_details, "show_price_comparison": show_price_comparison, "filter_cloud_provider": filter_cloud_provider, "filter_service": filter_service, "filter_compute_plan_group": filter_compute_plan_group, "filter_service_level": filter_service_level, "all_cloud_providers": all_cloud_providers, "all_services": all_services, "all_compute_plan_groups": all_compute_plan_groups, "all_service_levels": all_service_levels, } return render(request, "services/pricelist.html", context)