/** * 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;