diff --git a/hub/services/static/js/price-calculator.js b/hub/services/static/js/price-calculator.js
index 30a581a..377c097 100644
--- a/hub/services/static/js/price-calculator.js
+++ b/hub/services/static/js/price-calculator.js
@@ -335,9 +335,18 @@ Please contact me with next steps for ordering this configuration.`;
this.cpuValue.textContent = selectedPlan.vcpus;
this.memoryValue.textContent = selectedPlan.ram;
+ // Fade out CPU and Memory sliders since plan is manually selected
+ this.fadeOutSliders(['cpu', 'memory']);
+
+ // Update addons for the new configuration
this.updateAddons();
+ // Update pricing with the selected plan
this.updatePricingWithPlan(selectedPlan);
} else {
+ // Auto-select mode - fade sliders back in
+ this.fadeInSliders(['cpu', 'memory']);
+
+ // Auto-select mode - update addons and recalculate
this.updateAddons();
this.updatePricing();
}
@@ -555,6 +564,8 @@ Please contact me with next steps for ordering this configuration.`;
if (!addon.is_mandatory) {
const checkbox = addonElement.querySelector('.addon-checkbox');
checkbox.addEventListener('change', () => {
+ // Update addon prices and recalculate total
+ this.updateAddonPrices();
this.updatePricing();
});
}
@@ -571,7 +582,7 @@ Please contact me with next steps for ordering this configuration.`;
const storage = parseInt(this.storageRange?.value || 20);
const instances = parseInt(this.instancesRange?.value || 1);
- // Find the current plan data to get variable_unit
+ // Find the current plan data to get variable_unit for addon calculations
const matchedPlan = this.getCurrentPlan();
const variableUnit = matchedPlan?.variable_unit || 'CPU';
const units = variableUnit === 'CPU' ? cpus : memory;
@@ -583,17 +594,22 @@ Please contact me with next steps for ordering this configuration.`;
const priceElement = checkbox.parentElement.querySelector('.addon-price-value');
let calculatedPrice = 0;
+
+ // Calculate addon price based on type
if (addon.addon_type === 'BASE_FEE') {
+ // Base fee: price per instance
calculatedPrice = parseFloat(addon.price || 0) * instances;
} else if (addon.addon_type === 'UNIT_RATE') {
+ // Unit rate: price per unit (CPU or memory) across all instances
calculatedPrice = parseFloat(addon.price_per_unit || 0) * totalUnits;
}
+ // Update the display price
if (priceElement) {
priceElement.textContent = calculatedPrice.toFixed(2);
}
- // Update the checkbox data for later calculation
+ // Store the calculated price for later use in total calculations
checkbox.dataset.calculatedPrice = calculatedPrice.toString();
});
}
@@ -665,7 +681,7 @@ Please contact me with next steps for ordering this configuration.`;
updatePricing() {
if (!this.pricingData || !this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return;
- // Update addon prices first
+ // Update addon prices first to ensure they're current
this.updateAddonPrices();
// Reset plan selection if in auto-select mode
@@ -690,7 +706,13 @@ Please contact me with next steps for ordering this configuration.`;
} else {
// Plan is directly selected, update storage pricing
const selectedPlan = JSON.parse(this.planSelect.value);
- this.updatePricingWithPlan(selectedPlan);
+ const storage = parseInt(this.storageRange.value);
+ const instances = parseInt(this.instancesRange.value);
+
+ // Update addon prices for current configuration
+ this.updateAddonPrices();
+ this.showPlanDetails(selectedPlan, storage, instances);
+ this.updateStatusMessage('Plan selected directly!', 'success');
}
}
@@ -715,13 +737,18 @@ Please contact me with next steps for ordering this configuration.`;
if (this.planInstances) this.planInstances.textContent = instances;
if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel;
+ // Ensure addon prices are calculated with current configuration
+ this.updateAddonPrices();
+
// Calculate pricing using final price from plan data (which already includes mandatory addons)
// plan.final_price = compute_plan_price + sla_price (where sla_price includes mandatory addons)
const managedServicePricePerInstance = parseFloat(plan.final_price);
- // Collect mandatory addons for display (but don't add to price since they're already included)
+ // Collect addon information for display and calculation
let mandatoryAddonTotal = 0;
+ let optionalAddonTotal = 0;
const mandatoryAddons = [];
+ const selectedOptionalAddons = [];
if (this.addonsContainer) {
const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox');
@@ -730,11 +757,19 @@ Please contact me with next steps for ordering this configuration.`;
const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0);
if (addon.is_mandatory) {
- // Don't add to mandatoryAddonTotal since it's already in plan.final_price
+ // Mandatory addons are already included in plan.final_price
+ // We collect them for display purposes only
mandatoryAddons.push({
name: addon.name,
price: calculatedPrice.toFixed(2)
});
+ } else if (checkbox.checked) {
+ // Only count checked optional addons
+ optionalAddonTotal += calculatedPrice;
+ selectedOptionalAddons.push({
+ name: addon.name,
+ price: calculatedPrice.toFixed(2)
+ });
}
});
}
@@ -745,26 +780,7 @@ Please contact me with next steps for ordering this configuration.`;
const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice;
const storagePriceValue = storage * storageUnitPrice * instances;
- // Calculate optional addon total
- let optionalAddonTotal = 0;
- const selectedOptionalAddons = [];
-
- if (this.addonsContainer) {
- const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox:checked');
- addonCheckboxes.forEach(checkbox => {
- const addon = JSON.parse(checkbox.dataset.addon);
- const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0);
-
- if (!addon.is_mandatory) {
- optionalAddonTotal += calculatedPrice;
- selectedOptionalAddons.push({
- name: addon.name,
- price: calculatedPrice.toFixed(2)
- });
- }
- });
- }
-
+ // Total price = managed service price (includes mandatory addons) + storage + optional addons
const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal;
// Update pricing display
@@ -797,20 +813,33 @@ Please contact me with next steps for ordering this configuration.`;
// Clear existing addon pricing display
this.addonPricingContainer.innerHTML = '';
- // Add mandatory addons to pricing breakdown
+ // Add mandatory addons to pricing breakdown (for informational purposes only)
if (mandatoryAddons && mandatoryAddons.length > 0) {
+ // Add a note explaining mandatory addons are included
+ const mandatoryNote = document.createElement('div');
+ mandatoryNote.className = 'text-muted small mb-2';
+ mandatoryNote.innerHTML = 'Required add-ons (included in managed service price):';
+ this.addonPricingContainer.appendChild(mandatoryNote);
+
mandatoryAddons.forEach(addon => {
const addonRow = document.createElement('div');
- addonRow.className = 'd-flex justify-content-between mb-2';
+ addonRow.className = 'd-flex justify-content-between mb-1 ps-3';
addonRow.innerHTML = `
- Add-on: ${addon.name} (Required)
- CHF ${addon.price}
+ ${addon.name}
+ CHF ${addon.price}
`;
this.addonPricingContainer.appendChild(addonRow);
});
+
+ // Add separator if there are also optional addons
+ if (selectedOptionalAddons && selectedOptionalAddons.length > 0) {
+ const separator = document.createElement('hr');
+ separator.className = 'my-2';
+ this.addonPricingContainer.appendChild(separator);
+ }
}
- // Add optional addons to pricing breakdown
+ // Add optional addons to pricing breakdown (these are added to total)
if (selectedOptionalAddons && selectedOptionalAddons.length > 0) {
selectedOptionalAddons.forEach(addon => {
const addonRow = document.createElement('div');
@@ -852,6 +881,58 @@ Please contact me with next steps for ordering this configuration.`;
this.planMatchStatus.style.display = 'block';
}
}
+
+ // Fade out specified sliders when plan is manually selected
+ fadeOutSliders(sliderTypes) {
+ sliderTypes.forEach(type => {
+ const sliderContainer = this.getSliderContainer(type);
+ if (sliderContainer) {
+ sliderContainer.style.transition = 'opacity 0.3s ease-in-out';
+ sliderContainer.style.opacity = '0.3';
+ sliderContainer.style.pointerEvents = 'none';
+
+ // Add visual indicator that sliders are disabled
+ const slider = sliderContainer.querySelector('.form-range');
+ if (slider) {
+ slider.style.cursor = 'not-allowed';
+ }
+ }
+ });
+ }
+
+ // Fade in specified sliders when auto-select mode is chosen
+ fadeInSliders(sliderTypes) {
+ sliderTypes.forEach(type => {
+ const sliderContainer = this.getSliderContainer(type);
+ if (sliderContainer) {
+ sliderContainer.style.transition = 'opacity 0.3s ease-in-out';
+ sliderContainer.style.opacity = '1';
+ sliderContainer.style.pointerEvents = 'auto';
+
+ // Remove visual indicator
+ const slider = sliderContainer.querySelector('.form-range');
+ if (slider) {
+ slider.style.cursor = 'pointer';
+ }
+ }
+ });
+ }
+
+ // Get slider container element by type
+ getSliderContainer(type) {
+ switch (type) {
+ case 'cpu':
+ return this.cpuRange?.closest('.mb-4');
+ case 'memory':
+ return this.memoryRange?.closest('.mb-4');
+ case 'storage':
+ return this.storageRange?.closest('.mb-4');
+ case 'instances':
+ return this.instancesRange?.closest('.mb-4');
+ default:
+ return null;
+ }
+ }
}
// Initialize calculator when DOM is loaded
diff --git a/hub/services/views/pricelist.py b/hub/services/views/pricelist.py
index 2616522..b392f6b 100644
--- a/hub/services/views/pricelist.py
+++ b/hub/services/views/pricelist.py
@@ -203,13 +203,56 @@ def pricelist(request):
discount_savings = 0
discount_percentage = 0
- # Get addon information
- addons = appcat_price.addons.filter(active=True)
+ # 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)
+
+ # 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 = base_sla_price - sla_price
+ discount_percentage = (
+ (discount_savings / base_sla_price) * 100
+ if base_sla_price > 0
+ else 0
+ )
+ discount_breakdown = (
+ appcat_price.discount_model.get_discount_breakdown(
+ unit_rate, total_units
+ )
+ )
+ else:
+ sla_price = base_sla_price
+ discounted_price = total_units * unit_rate
+ discount_savings = 0
+ discount_percentage = 0
+
+ # Extract addon information from the calculation
mandatory_addons = []
optional_addons = []
- # Group addons by mandatory vs optional
- for addon in addons:
+ # Get all addons to separate mandatory from optional
+ all_addons = appcat_price.addons.filter(active=True)
+ for addon in all_addons:
addon_price = None
if addon.addon_type == "BF": # Base Fee
@@ -232,12 +275,12 @@ def pricelist(request):
if addon.mandatory:
mandatory_addons.append(addon_info)
- if addon_price:
- sla_price += addon_price
else:
optional_addons.append(addon_info)
- final_price = compute_plan_price + sla_price
+ # Use the calculated total price which includes mandatory addons
+ service_price_with_addons = price_calculation["total_price"]
+ final_price = compute_plan_price + service_price_with_addons
service_level_display = dict(VSHNAppCatPrice.ServiceLevel.choices)[
service_level
]
@@ -309,8 +352,8 @@ def pricelist(request):
"service_level": service_level_display,
"sla_base": base_fee,
"sla_per_unit": unit_rate,
- "sla_price": sla_price,
- "standard_sla_price": standard_sla_price,
+ "sla_price": service_price_with_addons,
+ "standard_sla_price": base_sla_price,
"discounted_sla_price": (
base_fee + discounted_price
if appcat_price.discount_model