145 lines
4.7 KiB
JavaScript
145 lines
4.7 KiB
JavaScript
/**
|
||
* Rental Duration Calculator with Hybrid Pricing
|
||
*
|
||
* Finds the optimal (cheapest) combination of pricing tiers for any rental duration.
|
||
* Supports combining hours + days + weeks + months for best pricing.
|
||
*
|
||
* Example: 26 hours with $10/hr and $50/day
|
||
* - Old logic: 2 days × $50 = $100
|
||
* - New logic: 1 day × $50 + 2 hours × $10 = $70
|
||
*/
|
||
class RentalDurationCalculator {
|
||
// Time constants in milliseconds
|
||
static HOUR_MS = 60 * 60 * 1000;
|
||
static DAY_MS = 24 * 60 * 60 * 1000;
|
||
static WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||
static MONTH_MS = 30 * 24 * 60 * 60 * 1000;
|
||
|
||
/**
|
||
* Build available pricing tiers from item, sorted largest to smallest
|
||
* @param {Object} item - Item object with pricing information
|
||
* @returns {Array} Array of {name, price, durationMs} sorted largest to smallest
|
||
*/
|
||
static buildPricingTiers(item) {
|
||
const tiers = [];
|
||
|
||
if (item.pricePerMonth && Number(item.pricePerMonth) > 0) {
|
||
tiers.push({
|
||
name: "month",
|
||
price: Number(item.pricePerMonth),
|
||
durationMs: this.MONTH_MS,
|
||
});
|
||
}
|
||
|
||
if (item.pricePerWeek && Number(item.pricePerWeek) > 0) {
|
||
tiers.push({
|
||
name: "week",
|
||
price: Number(item.pricePerWeek),
|
||
durationMs: this.WEEK_MS,
|
||
});
|
||
}
|
||
|
||
if (item.pricePerDay && Number(item.pricePerDay) > 0) {
|
||
tiers.push({
|
||
name: "day",
|
||
price: Number(item.pricePerDay),
|
||
durationMs: this.DAY_MS,
|
||
});
|
||
}
|
||
|
||
if (item.pricePerHour && Number(item.pricePerHour) > 0) {
|
||
tiers.push({
|
||
name: "hour",
|
||
price: Number(item.pricePerHour),
|
||
durationMs: this.HOUR_MS,
|
||
});
|
||
}
|
||
|
||
return tiers;
|
||
}
|
||
|
||
/**
|
||
* Recursively calculate optimal price for remaining duration
|
||
* Uses greedy approach: try largest tier first, then handle remainder with smaller tiers
|
||
* Compares hybrid price vs round-up price and returns the cheaper option
|
||
*
|
||
* @param {number} remainingMs - Remaining duration in milliseconds
|
||
* @param {Array} tiers - Available pricing tiers (largest to smallest)
|
||
* @param {number} tierIndex - Current tier being considered
|
||
* @returns {number} Optimal price for this duration
|
||
*/
|
||
static calculateOptimalForDuration(remainingMs, tiers, tierIndex = 0) {
|
||
// Base case: no remaining time
|
||
if (remainingMs <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
// Base case: no more tiers available - round up with smallest available tier
|
||
if (tierIndex >= tiers.length) {
|
||
const smallestTier = tiers[tiers.length - 1];
|
||
const unitsNeeded = Math.ceil(remainingMs / smallestTier.durationMs);
|
||
return unitsNeeded * smallestTier.price;
|
||
}
|
||
|
||
const currentTier = tiers[tierIndex];
|
||
const completeUnits = Math.floor(remainingMs / currentTier.durationMs);
|
||
const remainder = remainingMs - completeUnits * currentTier.durationMs;
|
||
|
||
if (completeUnits > 0) {
|
||
// Option 1: Use complete units of current tier + handle remainder with smaller tiers
|
||
const currentCost = completeUnits * currentTier.price;
|
||
const remainderCost = this.calculateOptimalForDuration(
|
||
remainder,
|
||
tiers,
|
||
tierIndex + 1
|
||
);
|
||
const hybridCost = currentCost + remainderCost;
|
||
|
||
// Option 2: Round up to one more unit of current tier (may be cheaper if remainder cost is high)
|
||
const roundUpCost = (completeUnits + 1) * currentTier.price;
|
||
|
||
// Return the cheaper option when there's a remainder to handle
|
||
return remainder > 0 ? Math.min(hybridCost, roundUpCost) : currentCost;
|
||
} else {
|
||
// Duration too small for this tier, try next smaller tier
|
||
return this.calculateOptimalForDuration(remainingMs, tiers, tierIndex + 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calculate rental cost based on duration and item pricing tiers
|
||
* Uses hybrid pricing to find the optimal (cheapest) combination of tiers
|
||
*
|
||
* @param {Date|string} startDateTime - Rental start date/time
|
||
* @param {Date|string} endDateTime - Rental end date/time
|
||
* @param {Object} item - Item object with pricing information
|
||
* @returns {number} Total rental cost
|
||
*/
|
||
static calculateRentalCost(startDateTime, endDateTime, item) {
|
||
const start = new Date(startDateTime);
|
||
const end = new Date(endDateTime);
|
||
|
||
// Calculate total duration in milliseconds
|
||
const durationMs = end.getTime() - start.getTime();
|
||
|
||
if (durationMs <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
// Build available pricing tiers
|
||
const tiers = this.buildPricingTiers(item);
|
||
|
||
// No pricing tiers = free item
|
||
if (tiers.length === 0) {
|
||
return 0;
|
||
}
|
||
|
||
// Calculate optimal price using hybrid tier combination
|
||
const optimalPrice = this.calculateOptimalForDuration(durationMs, tiers);
|
||
|
||
return parseFloat(optimalPrice.toFixed(2));
|
||
}
|
||
}
|
||
|
||
module.exports = RentalDurationCalculator;
|