Files
rentall-app/backend/utils/rentalDurationCalculator.js

145 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;