Edited layout of mmddyyyy and time dropdown. Changed algorithm for determining pricing so that it choosest the cheapest option for users
This commit is contained in:
@@ -1,61 +1,143 @@
|
||||
/**
|
||||
* 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 rentalStartDateTime = new Date(startDateTime);
|
||||
const rentalEndDateTime = new Date(endDateTime);
|
||||
const start = new Date(startDateTime);
|
||||
const end = new Date(endDateTime);
|
||||
|
||||
// Calculate rental duration
|
||||
const diffMs = rentalEndDateTime.getTime() - rentalStartDateTime.getTime();
|
||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
const diffWeeks = Math.ceil(diffDays / 7);
|
||||
// Calculate total duration in milliseconds
|
||||
const durationMs = end.getTime() - start.getTime();
|
||||
|
||||
// Calculate difference in calendar months
|
||||
let diffMonths = (rentalEndDateTime.getFullYear() - rentalStartDateTime.getFullYear()) * 12;
|
||||
diffMonths += rentalEndDateTime.getMonth() - rentalStartDateTime.getMonth();
|
||||
|
||||
// Add 1 if we're past the start day/time in the end month
|
||||
if (rentalEndDateTime.getDate() > rentalStartDateTime.getDate()) {
|
||||
diffMonths += 1;
|
||||
} else if (rentalEndDateTime.getDate() === rentalStartDateTime.getDate() &&
|
||||
rentalEndDateTime.getTime() > rentalStartDateTime.getTime()) {
|
||||
diffMonths += 1;
|
||||
if (durationMs <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
diffMonths = Math.max(1, diffMonths);
|
||||
// Build available pricing tiers
|
||||
const tiers = this.buildPricingTiers(item);
|
||||
|
||||
// Calculate base amount based on duration (tiered pricing)
|
||||
let totalAmount;
|
||||
|
||||
if (item.pricePerHour && diffHours < 24) {
|
||||
// Use hourly rate for rentals under 24 hours
|
||||
totalAmount = diffHours * Number(item.pricePerHour);
|
||||
} else if (diffDays <= 7 && item.pricePerDay) {
|
||||
// Use daily rate for rentals <= 7 days
|
||||
totalAmount = diffDays * Number(item.pricePerDay);
|
||||
} else if (diffMonths <= 1 && item.pricePerWeek) {
|
||||
// Use weekly rate for rentals <= 1 calendar month
|
||||
totalAmount = diffWeeks * Number(item.pricePerWeek);
|
||||
} else if (diffMonths > 1 && item.pricePerMonth) {
|
||||
// Use monthly rate for rentals > 1 calendar month
|
||||
totalAmount = diffMonths * Number(item.pricePerMonth);
|
||||
} else if (item.pricePerWeek) {
|
||||
// Fallback to weekly rate if monthly not available
|
||||
totalAmount = diffWeeks * Number(item.pricePerWeek);
|
||||
} else if (item.pricePerDay) {
|
||||
// Fallback to daily rate
|
||||
totalAmount = diffDays * Number(item.pricePerDay);
|
||||
} else {
|
||||
totalAmount = 0;
|
||||
// No pricing tiers = free item
|
||||
if (tiers.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return totalAmount;
|
||||
// Calculate optimal price using hybrid tier combination
|
||||
const optimalPrice = this.calculateOptimalForDuration(durationMs, tiers);
|
||||
|
||||
return parseFloat(optimalPrice.toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user