/** * Price Calculator - Main orchestrator class * Coordinates all the different managers to provide pricing calculation functionality */ class PriceCalculator { constructor() { try { // Initialize managers this.domManager = new DOMManager(); this.currentOffering = this.extractOfferingFromURL(); if (!this.currentOffering) { throw new Error('Unable to extract offering information from URL'); } this.pricingDataManager = new PricingDataManager(this.currentOffering); this.planManager = new PlanManager(this.pricingDataManager); this.addonManager = new AddonManager(this.pricingDataManager); this.uiManager = new UIManager(); this.orderManager = new OrderManager(); // Initialize the calculator this.init(); } catch (error) { console.error('Error initializing PriceCalculator:', error); this.showInitializationError(error.message); } } // Extract offering info from URL extractOfferingFromURL() { const pathParts = window.location.pathname.split('/'); if (pathParts.length >= 4 && pathParts[1] === 'offering') { return { provider_slug: pathParts[2], service_slug: pathParts[3] }; } return null; } // Initialize calculator async init() { try { // Load pricing data and setup calculator if (this.currentOffering) { await this.pricingDataManager.loadPricingData(); this.setupEventListeners(); this.setupUI(); this.orderManager.setupOrderButton(this.domManager); this.updateCalculator(); } else { throw new Error('No current offering found'); } } catch (error) { console.error('Error initializing price calculator:', error); this.showInitializationError(error.message); } } // Show initialization error to user showInitializationError(message) { const planMatchStatus = this.domManager?.get('planMatchStatus'); if (planMatchStatus) { planMatchStatus.innerHTML = ` Failed to load pricing calculator: ${message} `; planMatchStatus.className = 'alert alert-danger mb-3'; planMatchStatus.style.display = 'block'; } } // Setup initial UI components setupUI() { // Setup service levels based on available data this.uiManager.setupServiceLevels(this.domManager, this.pricingDataManager); // Calculate and set slider maximums and ranges this.uiManager.updateSliderMaximums(this.domManager, this.pricingDataManager); // Set smart default values based on available plans this.domManager.setSmartDefaults(this.pricingDataManager); // Populate plan dropdown this.planManager.populatePlanDropdown(this.domManager); // Initialize instances slider this.uiManager.updateInstancesSlider(this.domManager, this.pricingDataManager); } // Setup event listeners for calculator controls setupEventListeners() { const cpuRange = this.domManager.get('cpuRange'); const memoryRange = this.domManager.get('memoryRange'); const storageRange = this.domManager.get('storageRange'); const instancesRange = this.domManager.get('instancesRange'); if (!cpuRange || !memoryRange || !storageRange || !instancesRange) return; // Slider event listeners cpuRange.addEventListener('input', () => { this.domManager.get('cpuValue').textContent = cpuRange.value; // Only synchronize if in auto-select mode (no manual plan selection) const planSelect = this.domManager.get('planSelect'); if (!planSelect?.value) { this.synchronizeMemoryToMatchingPlan(parseFloat(cpuRange.value)); } this.updatePricing(); }); memoryRange.addEventListener('input', () => { this.domManager.get('memoryValue').textContent = memoryRange.value; // Only synchronize if in auto-select mode (no manual plan selection) const planSelect = this.domManager.get('planSelect'); if (!planSelect?.value) { this.synchronizeCpuToMatchingPlan(parseFloat(memoryRange.value)); } this.updatePricing(); }); storageRange.addEventListener('input', () => { this.domManager.get('storageValue').textContent = storageRange.value; this.updatePricing(); }); instancesRange.addEventListener('input', () => { this.domManager.get('instancesValue').textContent = instancesRange.value; this.updatePricing(); }); // Plan selection listener const planSelect = this.domManager.get('planSelect'); if (planSelect) { planSelect.addEventListener('change', () => { if (planSelect.value) { const selectedPlan = JSON.parse(planSelect.value); // Update sliders to match selected plan this.planManager.updateSlidersForPlan(selectedPlan, this.domManager); // Fade out CPU and Memory sliders since plan is manually selected this.uiManager.fadeOutSliders(this.domManager, ['cpu', 'memory']); // Update addons for the new configuration this.addonManager.updateAddons(this.domManager); // Update pricing with the selected plan this.updatePricingWithPlan(selectedPlan); } else { // Auto-select mode - reset sliders to smart default values this.domManager.setSmartDefaults(this.pricingDataManager); // Auto-select mode - fade sliders back in this.uiManager.fadeInSliders(this.domManager, ['cpu', 'memory']); // Auto-select mode - update addons and recalculate this.addonManager.updateAddons(this.domManager); this.updatePricing(); } }); } // Listen for addon changes window.addEventListener('addon-changed', () => { this.updatePricing(); }); // Service level change listener const serviceLevelInputs = this.domManager.get('serviceLevelInputs'); if (serviceLevelInputs) { serviceLevelInputs.forEach(input => { input.addEventListener('change', () => { // Update plan dropdown for new service level this.planManager.populatePlanDropdown(this.domManager); // Update addons for new service level this.addonManager.updateAddons(this.domManager); // Update pricing this.updatePricing(); }); }); } } // Update calculator (initial setup) updateCalculator() { this.addonManager.updateAddons(this.domManager); this.updatePricing(); } // Update pricing with specific plan updatePricingWithPlan(selectedPlan) { const config = this.domManager.getCurrentConfiguration(); // Update addon prices first to ensure calculated prices are current this.addonManager.updateAddonPrices(this.domManager, this.planManager); this.showPlanDetails(selectedPlan, config.storage, config.instances); this.uiManager.updateStatusMessage(this.domManager, 'Plan selected directly!', 'success'); } // Main pricing update function updatePricing() { // Update addon prices first to ensure they're current this.addonManager.updateAddonPrices(this.domManager, this.planManager); const planSelect = this.domManager.get('planSelect'); // Reset plan selection if in auto-select mode if (!planSelect?.value) { const config = this.domManager.getCurrentConfiguration(); if (!config.serviceLevel) { return; } // Find best matching plan const matchedPlan = this.planManager.findBestMatchingPlan(config.cpus, config.memory, config.serviceLevel); if (matchedPlan) { this.showPlanDetails(matchedPlan, config.storage, config.instances); this.uiManager.updateStatusMessage(this.domManager, 'Perfect match found!', 'success'); } else { this.uiManager.showNoMatch(this.domManager); } } else { // Plan is directly selected, update storage pricing const selectedPlan = JSON.parse(planSelect.value); const config = this.domManager.getCurrentConfiguration(); // Update addon prices for current configuration this.addonManager.updateAddonPrices(this.domManager, this.planManager); this.showPlanDetails(selectedPlan, config.storage, config.instances); this.uiManager.updateStatusMessage(this.domManager, 'Plan selected directly!', 'success'); } } // Show plan details in the UI showPlanDetails(plan, storage, instances) { // Get current service level const serviceLevel = this.domManager.getSelectedServiceLevel() || 'Best Effort'; // Ensure addon prices are calculated with current configuration this.addonManager.updateAddonPrices(this.domManager, this.planManager); // Calculate pricing using final price from plan data (which already includes mandatory addons) const managedServicePricePerInstance = parseFloat(plan.final_price); // Collect addon information for display and calculation const addons = this.addonManager.getSelectedAddons(this.domManager); const optionalAddonTotal = this.addonManager.calculateOptionalAddonTotal(this.domManager); 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.pricingDataManager.getStoragePrice(); const storagePriceValue = storage * storageUnitPrice * instances; // Total price = managed service price (includes mandatory addons) + storage + optional addons const totalPriceValue = managedServicePrice + storagePriceValue + optionalAddonTotal; // Show plan details in UI this.uiManager.showPlanDetails( this.domManager, plan, storage, instances, serviceLevel, managedServicePrice, storagePriceValue, totalPriceValue ); // Update addon pricing display this.uiManager.updateAddonPricingDisplay(this.domManager, addons.mandatory, addons.optional); // Store current configuration for order button this.orderManager.storeConfiguration( plan, { storage, instances }, serviceLevel, totalPriceValue.toFixed(2), [...addons.mandatory, ...addons.optional] ); } // Synchronize memory slider to match CPU value with best matching plan synchronizeMemoryToMatchingPlan(targetCpu) { const serviceLevel = this.domManager.getSelectedServiceLevel(); if (!serviceLevel) return; // Get all available plans for the current service level const availablePlans = this.pricingDataManager.getPlansForServiceLevel(serviceLevel); if (!availablePlans || availablePlans.length === 0) return; // Snap CPU to nearest available value first const { cpuValues } = this.pricingDataManager.getAvailableSliderValues(); const snappedCpu = this.findNearestValue(targetCpu, cpuValues); // Update CPU slider to snapped value if different if (snappedCpu !== targetCpu) { const cpuRange = this.domManager.get('cpuRange'); const cpuValue = this.domManager.get('cpuValue'); if (cpuRange && cpuValue) { cpuRange.value = snappedCpu; cpuValue.textContent = snappedCpu; } } // Find the plan that best matches the snapped CPU requirement let bestPlan = null; let minDifference = Infinity; availablePlans.forEach(plan => { const planCpu = parseFloat(plan.vcpus); // Look for plans that meet or exceed the CPU requirement if (planCpu >= snappedCpu) { const difference = planCpu - snappedCpu; if (difference < minDifference) { minDifference = difference; bestPlan = plan; } } }); // If no plan meets the CPU requirement, find the closest one below it if (!bestPlan) { availablePlans.forEach(plan => { const planCpu = parseFloat(plan.vcpus); const difference = Math.abs(planCpu - snappedCpu); if (difference < minDifference) { minDifference = difference; bestPlan = plan; } }); } // Update memory slider to match the found plan if (bestPlan) { const memoryRange = this.domManager.get('memoryRange'); const memoryValue = this.domManager.get('memoryValue'); if (memoryRange && memoryValue) { memoryRange.value = bestPlan.ram; memoryValue.textContent = bestPlan.ram; } } } // Synchronize CPU slider to match memory value with best matching plan synchronizeCpuToMatchingPlan(targetMemory) { const serviceLevel = this.domManager.getSelectedServiceLevel(); if (!serviceLevel) return; // Get all available plans for the current service level const availablePlans = this.pricingDataManager.getPlansForServiceLevel(serviceLevel); if (!availablePlans || availablePlans.length === 0) return; // Snap memory to nearest available value first const { memoryValues } = this.pricingDataManager.getAvailableSliderValues(); const snappedMemory = this.findNearestValue(targetMemory, memoryValues); // Update memory slider to snapped value if different if (snappedMemory !== targetMemory) { const memoryRange = this.domManager.get('memoryRange'); const memoryValue = this.domManager.get('memoryValue'); if (memoryRange && memoryValue) { memoryRange.value = snappedMemory; memoryValue.textContent = snappedMemory; } } // Find the plan that best matches the snapped memory requirement let bestPlan = null; let minDifference = Infinity; availablePlans.forEach(plan => { const planMemory = parseFloat(plan.ram); // Look for plans that meet or exceed the memory requirement if (planMemory >= snappedMemory) { const difference = planMemory - snappedMemory; if (difference < minDifference) { minDifference = difference; bestPlan = plan; } } }); // If no plan meets the memory requirement, find the closest one below it if (!bestPlan) { availablePlans.forEach(plan => { const planMemory = parseFloat(plan.ram); const difference = Math.abs(planMemory - snappedMemory); if (difference < minDifference) { minDifference = difference; bestPlan = plan; } }); } // Update CPU slider to match the found plan if (bestPlan) { const cpuRange = this.domManager.get('cpuRange'); const cpuValue = this.domManager.get('cpuValue'); if (cpuRange && cpuValue) { cpuRange.value = bestPlan.vcpus; cpuValue.textContent = bestPlan.vcpus; } } } // Find the nearest value in an array to a target value findNearestValue(target, availableValues) { if (!availableValues || availableValues.length === 0) return target; let nearest = availableValues[0]; let minDifference = Math.abs(target - nearest); for (let i = 1; i < availableValues.length; i++) { const difference = Math.abs(target - availableValues[i]); if (difference < minDifference) { minDifference = difference; nearest = availableValues[i]; } } return nearest; } } // Export for use in other modules window.PriceCalculator = PriceCalculator;