still buggy price calculator

This commit is contained in:
Tobias Brunner 2025-06-19 17:05:49 +02:00
parent 22e527bcd9
commit 9d423ce61e
No known key found for this signature in database
3 changed files with 268 additions and 350 deletions

View file

@ -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 = `
<div class="form-check">
<input class="form-check-input addon-checkbox"
type="checkbox"
id="addon-${addon.id}"
value="${addon.id}"
data-addon='${JSON.stringify(addon)}'
${addon.is_mandatory ? 'checked disabled' : ''}>
<label class="form-check-label" for="addon-${addon.id}">
<strong>${addon.name}</strong>
<div class="text-muted small">${addon.commercial_description || ''}</div>
<div class="text-primary addon-price-display">
${addon.is_mandatory ? 'Required - ' : ''}CHF <span class="addon-price-value">0.00</span>
</div>
</label>
</div>
`;
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 = `
<span>Add-on: ${addon.name} <small class="text-muted">(Required)</small></span>
<span class="fw-bold text-muted">CHF ${addon.price}</span>
`;
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 = `
<span>Add-on: ${addon.name}</span>
<span class="fw-bold">CHF ${addon.price}</span>
`;
this.addonPricingContainer.appendChild(addonRow);
});
}
}
// Show no matching plan found
showNoMatch() {
if (this.planMatchStatus) this.planMatchStatus.style.display = 'none';