From 9d423ce61e32aa1f2cb5297e24bbf7710a19a2bd Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 19 Jun 2025 17:05:49 +0200 Subject: [PATCH] still buggy price calculator --- hub/services/static/js/price-calculator.js | 273 +++++++++++++- .../templates/services/offering_detail.html | 340 +----------------- hub/services/views/offerings.py | 5 + 3 files changed, 268 insertions(+), 350 deletions(-) diff --git a/hub/services/static/js/price-calculator.js b/hub/services/static/js/price-calculator.js index a65a9f2..30a581a 100644 --- a/hub/services/static/js/price-calculator.js +++ b/hub/services/static/js/price-calculator.js @@ -10,6 +10,7 @@ class PriceCalculator { this.currentOffering = null; this.selectedConfiguration = null; this.replicaInfo = null; + this.addonsData = null; this.init(); } @@ -50,6 +51,10 @@ class PriceCalculator { this.serviceLevelInputs = document.querySelectorAll('input[name="serviceLevel"]'); this.planSelect = document.getElementById('planSelect'); + // Addon elements + this.addonsContainer = document.getElementById('addonsContainer'); + this.addonPricingContainer = document.getElementById('addonPricingContainer'); + // Result display elements this.planMatchStatus = document.getElementById('planMatchStatus'); this.selectedPlanDetails = document.getElementById('selectedPlanDetails'); @@ -156,25 +161,36 @@ class PriceCalculator { storage: config.storage, instances: config.instances, serviceLevel: config.serviceLevel, - totalPrice: config.totalPrice + totalPrice: config.totalPrice, + addons: config.addons || [] }); } } // Generate human-readable configuration message generateConfigurationMessage(config) { - return `I would like to order the following configuration: + let message = `I would like to order the following configuration: Plan: ${config.planName} (${config.planGroup}) vCPUs: ${config.vcpus} Memory: ${config.memory} GB Storage: ${config.storage} GB Instances: ${config.instances} -Service Level: ${config.serviceLevel} +Service Level: ${config.serviceLevel}`; -Total Monthly Price: CHF ${config.totalPrice} + // Add addons to the message if any are selected + if (config.addons && config.addons.length > 0) { + message += '\n\nSelected Add-ons:'; + config.addons.forEach(addon => { + message += `\n- ${addon.name}: CHF ${addon.price}`; + }); + } + + message += `\n\nTotal Monthly Price: CHF ${config.totalPrice} Please contact me with next steps for ordering this configuration.`; + + return message; } // Load pricing data from API endpoint @@ -185,13 +201,18 @@ Please contact me with next steps for ordering this configuration.`; throw new Error('Failed to load pricing data'); } - this.pricingData = await response.json(); + const data = await response.json(); + this.pricingData = data.pricing || data; + + // Extract addons data from the plans - addons are embedded in each plan + this.extractAddonsData(); // Extract storage price from the first available plan this.extractStoragePrice(); this.setupEventListeners(); this.populatePlanDropdown(); + this.updateAddons(); this.updatePricing(); } catch (error) { console.error('Error loading pricing data:', error); @@ -220,6 +241,50 @@ Please contact me with next steps for ordering this configuration.`; } } + // Extract addons data from pricing plans + extractAddonsData() { + if (!this.pricingData) return; + + this.addonsData = {}; + + // Extract addons from the first available plan for each service level + Object.keys(this.pricingData).forEach(groupName => { + const group = this.pricingData[groupName]; + Object.keys(group).forEach(serviceLevel => { + const plans = group[serviceLevel]; + if (plans.length > 0) { + // Use the first plan's addon data for this service level + const plan = plans[0]; + const allAddons = []; + + // Add mandatory addons + if (plan.mandatory_addons) { + plan.mandatory_addons.forEach(addon => { + allAddons.push({ + ...addon, + is_mandatory: true, + addon_type: addon.addon_type === "Base Fee" ? "BASE_FEE" : "UNIT_RATE" + }); + }); + } + + // Add optional addons + if (plan.optional_addons) { + plan.optional_addons.forEach(addon => { + allAddons.push({ + ...addon, + is_mandatory: false, + addon_type: addon.addon_type === "Base Fee" ? "BASE_FEE" : "UNIT_RATE" + }); + }); + } + + this.addonsData[serviceLevel] = allAddons; + } + }); + }); + } + // Setup event listeners for calculator controls setupEventListeners() { if (!this.cpuRange || !this.memoryRange || !this.storageRange || !this.instancesRange) return; @@ -253,6 +318,7 @@ Please contact me with next steps for ordering this configuration.`; input.addEventListener('change', () => { this.updateInstancesSlider(); this.populatePlanDropdown(); + this.updateAddons(); this.updatePricing(); }); }); @@ -269,8 +335,10 @@ Please contact me with next steps for ordering this configuration.`; this.cpuValue.textContent = selectedPlan.vcpus; this.memoryValue.textContent = selectedPlan.ram; + this.updateAddons(); this.updatePricingWithPlan(selectedPlan); } else { + this.updateAddons(); this.updatePricing(); } }); @@ -356,6 +424,7 @@ Please contact me with next steps for ordering this configuration.`; input.addEventListener('change', () => { this.updateInstancesSlider(); this.populatePlanDropdown(); + this.updateAddons(); this.updatePricing(); }); @@ -445,6 +514,103 @@ Please contact me with next steps for ordering this configuration.`; }); } + // Update addons based on current configuration + updateAddons() { + if (!this.addonsContainer || !this.addonsData) return; + + const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; + if (!serviceLevel || !this.addonsData[serviceLevel]) return; + + const addons = this.addonsData[serviceLevel]; + + // Clear existing addons + this.addonsContainer.innerHTML = ''; + + // Add each addon + addons.forEach(addon => { + const addonElement = document.createElement('div'); + addonElement.className = `addon-item mb-2 p-2 border rounded ${addon.is_mandatory ? 'bg-light' : ''}`; + + addonElement.innerHTML = ` +
+ + +
+ `; + + this.addonsContainer.appendChild(addonElement); + + // Add event listener for optional addons + if (!addon.is_mandatory) { + const checkbox = addonElement.querySelector('.addon-checkbox'); + checkbox.addEventListener('change', () => { + this.updatePricing(); + }); + } + }); + + // Update addon prices + this.updateAddonPrices(); + } // Update addon prices based on current configuration + updateAddonPrices() { + if (!this.addonsContainer) return; + + const cpus = parseInt(this.cpuRange?.value || 2); + const memory = parseInt(this.memoryRange?.value || 4); + const storage = parseInt(this.storageRange?.value || 20); + const instances = parseInt(this.instancesRange?.value || 1); + + // Find the current plan data to get variable_unit + const matchedPlan = this.getCurrentPlan(); + const variableUnit = matchedPlan?.variable_unit || 'CPU'; + const units = variableUnit === 'CPU' ? cpus : memory; + const totalUnits = units * instances; + + const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox'); + addonCheckboxes.forEach(checkbox => { + const addon = JSON.parse(checkbox.dataset.addon); + const priceElement = checkbox.parentElement.querySelector('.addon-price-value'); + + let calculatedPrice = 0; + if (addon.addon_type === 'BASE_FEE') { + calculatedPrice = parseFloat(addon.price || 0) * instances; + } else if (addon.addon_type === 'UNIT_RATE') { + calculatedPrice = parseFloat(addon.price_per_unit || 0) * totalUnits; + } + + if (priceElement) { + priceElement.textContent = calculatedPrice.toFixed(2); + } + + // Update the checkbox data for later calculation + checkbox.dataset.calculatedPrice = calculatedPrice.toString(); + }); + } + + // Get current plan based on configuration + getCurrentPlan() { + const cpus = parseInt(this.cpuRange?.value || 2); + const memory = parseInt(this.memoryRange?.value || 4); + const serviceLevel = document.querySelector('input[name="serviceLevel"]:checked')?.value; + + if (this.planSelect?.value) { + return JSON.parse(this.planSelect.value); + } + + return this.findBestMatchingPlan(cpus, memory, serviceLevel); + } + // Find best matching plan based on requirements findBestMatchingPlan(cpus, memory, serviceLevel) { if (!this.pricingData) return null; @@ -488,6 +654,9 @@ Please contact me with next steps for ordering this configuration.`; const storage = parseInt(this.storageRange?.value || 20); const instances = parseInt(this.instancesRange?.value || 1); + // Update addon prices first to ensure calculated prices are current + this.updateAddonPrices(); + this.showPlanDetails(selectedPlan, storage, instances); this.updateStatusMessage('Plan selected directly!', 'success'); } @@ -496,6 +665,9 @@ 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 + this.updateAddonPrices(); + // Reset plan selection if in auto-select mode if (!this.planSelect?.value) { const cpus = parseInt(this.cpuRange.value); @@ -543,16 +715,57 @@ Please contact me with next steps for ordering this configuration.`; if (this.planInstances) this.planInstances.textContent = instances; if (this.planServiceLevel) this.planServiceLevel.textContent = serviceLevel; - // Calculate pricing using storage price from the plan data - const computePriceValue = parseFloat(plan.compute_plan_price); - const servicePriceValue = parseFloat(plan.sla_price); - const managedServicePricePerInstance = computePriceValue + servicePriceValue; + // 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) + let mandatoryAddonTotal = 0; + const mandatoryAddons = []; + + if (this.addonsContainer) { + const addonCheckboxes = this.addonsContainer.querySelectorAll('.addon-checkbox'); + addonCheckboxes.forEach(checkbox => { + const addon = JSON.parse(checkbox.dataset.addon); + const calculatedPrice = parseFloat(checkbox.dataset.calculatedPrice || 0); + + if (addon.is_mandatory) { + // Don't add to mandatoryAddonTotal since it's already in plan.final_price + mandatoryAddons.push({ + name: addon.name, + price: calculatedPrice.toFixed(2) + }); + } + }); + } + const managedServicePrice = managedServicePricePerInstance * instances; // Use storage price from plan data or fallback to instance variable const storageUnitPrice = plan.storage_price !== undefined ? parseFloat(plan.storage_price) : this.storagePrice; const storagePriceValue = storage * storageUnitPrice * instances; - const totalPriceValue = managedServicePrice + storagePriceValue; + + // 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) + }); + } + }); + } + + const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal; // Update pricing display if (this.managedServicePrice) this.managedServicePrice.textContent = managedServicePrice.toFixed(2); @@ -560,6 +773,9 @@ Please contact me with next steps for ordering this configuration.`; if (this.storageAmount) this.storageAmount.textContent = storage; if (this.totalPrice) this.totalPrice.textContent = totalPriceValue.toFixed(2); + // Update addon pricing display + this.updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons); + // Store current configuration for order button this.selectedConfiguration = { planName: plan.compute_plan, @@ -569,10 +785,45 @@ Please contact me with next steps for ordering this configuration.`; storage: storage, instances: instances, serviceLevel: serviceLevel, - totalPrice: totalPriceValue.toFixed(2) + totalPrice: totalPriceValue.toFixed(2), + addons: [...mandatoryAddons, ...selectedOptionalAddons] }; } + // Update addon pricing display in the results panel + updateAddonPricingDisplay(mandatoryAddons, selectedOptionalAddons) { + if (!this.addonPricingContainer) return; + + // Clear existing addon pricing display + this.addonPricingContainer.innerHTML = ''; + + // Add mandatory addons to pricing breakdown + if (mandatoryAddons && mandatoryAddons.length > 0) { + mandatoryAddons.forEach(addon => { + const addonRow = document.createElement('div'); + addonRow.className = 'd-flex justify-content-between mb-2'; + addonRow.innerHTML = ` + Add-on: ${addon.name} (Required) + CHF ${addon.price} + `; + this.addonPricingContainer.appendChild(addonRow); + }); + } + + // Add optional addons to pricing breakdown + if (selectedOptionalAddons && selectedOptionalAddons.length > 0) { + selectedOptionalAddons.forEach(addon => { + const addonRow = document.createElement('div'); + addonRow.className = 'd-flex justify-content-between mb-2'; + addonRow.innerHTML = ` + Add-on: ${addon.name} + CHF ${addon.price} + `; + this.addonPricingContainer.appendChild(addonRow); + }); + } + } + // Show no matching plan found showNoMatch() { if (this.planMatchStatus) this.planMatchStatus.style.display = 'none'; diff --git a/hub/services/templates/services/offering_detail.html b/hub/services/templates/services/offering_detail.html index 1d5e5af..b8b5a61 100644 --- a/hub/services/templates/services/offering_detail.html +++ b/hub/services/templates/services/offering_detail.html @@ -271,7 +271,7 @@ - + @@ -460,342 +460,4 @@ - - - - {% endblock %} \ No newline at end of file diff --git a/hub/services/views/offerings.py b/hub/services/views/offerings.py index 8a2c0d1..fc5c59e 100644 --- a/hub/services/views/offerings.py +++ b/hub/services/views/offerings.py @@ -377,6 +377,7 @@ def generate_pricing_data(offering): for addon in addons: addon_price = None + addon_price_per_unit = None if addon.addon_type == "BF": # Base Fee addon_price = addon.get_price(currency) @@ -392,6 +393,7 @@ def generate_pricing_data(offering): "commercial_description": addon.commercial_description, "addon_type": addon.get_addon_type_display(), "price": addon_price, + "price_per_unit": addon_price_per_unit, # Add per-unit price for frontend calculations } if addon.mandatory: @@ -428,6 +430,9 @@ def generate_pricing_data(offering): "storage_price": storage_price_data.get(currency, 0), "ha_replica_min": appcat_price.ha_replica_min, "ha_replica_max": appcat_price.ha_replica_max, + "variable_unit": appcat_price.variable_unit, + "units": units, + "total_units": total_units, "mandatory_addons": mandatory_addons, "optional_addons": optional_addons, }