vpn-btcpay-provisioner/app/static/js/pricing.js

190 lines
6.4 KiB
JavaScript

// Constants for pricing
const HOURLY_RATE = 100; // 100 sats per hour
const MIN_HOURS = 1;
const MAX_HOURS = 2160; // 3 months
const MIN_SATS = HOURLY_RATE * MIN_HOURS;
const MAX_SATS = 216000; // Maximum for 3 months
// Utility functions for duration formatting
function formatDuration(hours) {
const exactHours = `${hours} hour${hours === 1 ? '' : 's'}`;
// Break down the time into components
const months = Math.floor(hours / 720);
const remainingAfterMonths = hours % 720;
const weeks = Math.floor(remainingAfterMonths / 168);
const remainingAfterWeeks = remainingAfterMonths % 168;
const days = Math.floor(remainingAfterWeeks / 24);
const remainingHours = remainingAfterWeeks % 24;
// Build the detailed breakdown
const parts = [];
if (months > 0) {
parts.push(`${months} month${months === 1 ? '' : 's'}`);
}
if (weeks > 0) {
parts.push(`${weeks} week${weeks === 1 ? '' : 's'}`);
}
if (days > 0) {
parts.push(`${days} day${days === 1 ? '' : 's'}`);
}
if (remainingHours > 0 || parts.length === 0) {
parts.push(`${remainingHours} hour${remainingHours === 1 ? '' : 's'}`);
}
// Combine all parts with proper grammar
let breakdown = '';
if (parts.length > 1) {
const lastPart = parts.pop();
breakdown = parts.join(', ') + ' and ' + lastPart;
} else {
breakdown = parts[0];
}
return `${exactHours} (${breakdown})`;
}
// Price calculation with volume discounts
function calculatePrice(hours) {
try {
hours = parseInt(hours);
if (hours < MIN_HOURS) return MIN_SATS;
let basePrice = hours * HOURLY_RATE;
if (hours >= 2160) { // 3 months
basePrice = basePrice * 0.75;
} else if (hours >= 720) { // 30 days
basePrice = basePrice * 0.85;
} else if (hours >= 168) { // 7 days
basePrice = basePrice * 0.90;
} else if (hours >= 24) { // 1 day
basePrice = basePrice * 0.95;
}
return Math.round(basePrice);
} catch (error) {
console.error('Error calculating price:', error);
return MIN_SATS;
}
}
// Calculate hours from price
function calculateHoursFromPrice(sats) {
try {
sats = parseInt(sats);
if (sats < MIN_SATS) return MIN_HOURS;
if (sats > MAX_SATS) return MAX_HOURS;
// Binary search for the closest hour value
const binarySearchHours = (min, max, targetSats) => {
while (min <= max) {
const mid = Math.floor((min + max) / 2);
const price = calculatePrice(mid);
if (price === targetSats) return mid;
if (price < targetSats) min = mid + 1;
else max = mid - 1;
}
return max;
};
let hours = 0;
if (sats >= calculatePrice(2160)) {
hours = Math.floor(sats / (HOURLY_RATE * 0.75));
} else if (sats >= calculatePrice(720)) {
hours = binarySearchHours(720, 2159, sats);
} else if (sats >= calculatePrice(168)) {
hours = binarySearchHours(168, 719, sats);
} else if (sats >= calculatePrice(24)) {
hours = binarySearchHours(24, 167, sats);
} else {
hours = binarySearchHours(1, 23, sats);
}
return Math.max(MIN_HOURS, Math.min(MAX_HOURS, hours));
} catch (error) {
console.error('Error calculating hours from price:', error);
return MIN_HOURS;
}
}
// Update all displays and inputs
function updateDisplays(hours, skipSource = null) {
const elements = {
priceDisplay: document.getElementById('price-display'),
durationDisplay: document.getElementById('duration-display'),
customHours: document.getElementById('custom-hours'),
customSats: document.getElementById('custom-sats')
};
hours = Math.max(MIN_HOURS, Math.min(MAX_HOURS, hours));
const price = calculatePrice(hours);
// Update displays
if (elements.priceDisplay && elements.durationDisplay) {
elements.priceDisplay.textContent = price;
elements.durationDisplay.textContent = formatDuration(hours);
}
// Update inputs (skip the source of the update)
if (skipSource !== 'hours' && elements.customHours) {
elements.customHours.value = hours;
}
if (skipSource !== 'sats' && elements.customSats) {
elements.customSats.value = price;
}
}
// Main pricing interface
export const Pricing = {
init() {
console.log('Initializing pricing system...');
const elements = {
customHours: document.getElementById('custom-hours'),
customSats: document.getElementById('custom-sats'),
presetButtons: document.querySelectorAll('.duration-preset')
};
// Initial display
updateDisplays(24); // Start with 24 hours as default
// Event listeners for custom inputs
elements.customHours?.addEventListener('input', (e) => {
let hours = parseInt(e.target.value) || MIN_HOURS;
hours = Math.max(MIN_HOURS, Math.min(MAX_HOURS, hours));
updateDisplays(hours, 'hours');
});
elements.customSats?.addEventListener('input', (e) => {
let sats = parseInt(e.target.value) || MIN_SATS;
sats = Math.max(MIN_SATS, Math.min(MAX_SATS, sats));
const hours = calculateHoursFromPrice(sats);
updateDisplays(hours, 'sats');
});
// Add blur events to enforce minimums
elements.customHours?.addEventListener('blur', (e) => {
if (!e.target.value || parseInt(e.target.value) < MIN_HOURS) {
updateDisplays(MIN_HOURS, 'hours');
}
});
elements.customSats?.addEventListener('blur', (e) => {
if (!e.target.value || parseInt(e.target.value) < MIN_SATS) {
updateDisplays(MIN_HOURS, 'sats');
}
});
// Handle preset buttons
elements.presetButtons.forEach(button => {
button.addEventListener('click', () => {
const hours = parseInt(button.getAttribute('data-hours'));
updateDisplays(hours);
});
});
}
};
// Auto-initialize on script load
document.addEventListener('DOMContentLoaded', () => {
Pricing.init();
});