377 lines
13 KiB
JavaScript
377 lines
13 KiB
JavaScript
const RentalDurationCalculator = require("../../../utils/rentalDurationCalculator");
|
|
|
|
describe("RentalDurationCalculator - Hybrid Pricing", () => {
|
|
// Helper to create ISO date strings
|
|
const date = (dateStr, time = "10:00") => {
|
|
return `2024-01-${dateStr.padStart(2, "0")}T${time}:00.000Z`;
|
|
};
|
|
|
|
describe("calculateRentalCost", () => {
|
|
describe("Basic single-tier pricing", () => {
|
|
test("should calculate hourly rate for short rentals", () => {
|
|
const item = { pricePerHour: 10 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("01", "13:00"),
|
|
item
|
|
);
|
|
expect(result).toBe(30); // 3 hours * $10
|
|
});
|
|
|
|
test("should calculate daily rate for exact day rentals", () => {
|
|
const item = { pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("03", "10:00"),
|
|
item
|
|
);
|
|
expect(result).toBe(100); // 2 days * $50
|
|
});
|
|
|
|
test("should calculate weekly rate for exact week rentals", () => {
|
|
const item = { pricePerWeek: 200 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("15", "10:00"),
|
|
item
|
|
);
|
|
expect(result).toBe(400); // 2 weeks * $200
|
|
});
|
|
|
|
test("should calculate monthly rate for exact month rentals", () => {
|
|
const item = { pricePerMonth: 600 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-03-01T10:00:00.000Z", // 60 days = exactly 2 months (using 30-day months)
|
|
item
|
|
);
|
|
expect(result).toBe(1200); // 2 months * $600
|
|
});
|
|
});
|
|
|
|
describe("Hybrid day + hours pricing", () => {
|
|
test("should combine day and hours for 26-hour rental", () => {
|
|
const item = { pricePerDay: 50, pricePerHour: 10 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "15:00"),
|
|
date("02", "17:00"), // 26 hours
|
|
item
|
|
);
|
|
expect(result).toBe(70); // 1 day ($50) + 2 hours ($20)
|
|
});
|
|
|
|
test("should combine day and hours for 27-hour rental", () => {
|
|
const item = { pricePerDay: 50, pricePerHour: 10 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("02", "13:00"), // 27 hours
|
|
item
|
|
);
|
|
expect(result).toBe(80); // 1 day ($50) + 3 hours ($30)
|
|
});
|
|
|
|
test("should choose cheaper option - round up when hourly is expensive", () => {
|
|
// 25 hours: daily = $40, hourly = $50
|
|
// Hybrid: 1 day ($40) + 1 hour ($50) = $90
|
|
// Round-up: 2 days ($80)
|
|
// Expected: $80 (round-up wins)
|
|
const item = { pricePerDay: 40, pricePerHour: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("02", "11:00"), // 25 hours
|
|
item
|
|
);
|
|
expect(result).toBe(80); // 2 days (cheaper than 1d + 1h)
|
|
});
|
|
|
|
test("should choose cheaper option - hybrid when hourly is cheap", () => {
|
|
// 25 hours: daily = $50, hourly = $5
|
|
// Hybrid: 1 day ($50) + 1 hour ($5) = $55
|
|
// Round-up: 2 days ($100)
|
|
// Expected: $55 (hybrid wins)
|
|
const item = { pricePerDay: 50, pricePerHour: 5 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("02", "11:00"), // 25 hours
|
|
item
|
|
);
|
|
expect(result).toBe(55); // 1 day + 1 hour (cheaper)
|
|
});
|
|
});
|
|
|
|
describe("Hybrid week + days pricing", () => {
|
|
test("should combine week and days for 10-day rental", () => {
|
|
const item = { pricePerWeek: 200, pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("11", "10:00"), // 10 days
|
|
item
|
|
);
|
|
expect(result).toBe(350); // 1 week ($200) + 3 days ($150)
|
|
});
|
|
|
|
test("should combine week and days for 9-day rental", () => {
|
|
const item = { pricePerWeek: 200, pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("10", "10:00"), // 9 days
|
|
item
|
|
);
|
|
expect(result).toBe(300); // 1 week ($200) + 2 days ($100)
|
|
});
|
|
|
|
test("should round up to 2 weeks when daily cost exceeds week", () => {
|
|
// 10 days: weekly = $100, daily = $50
|
|
// Hybrid: 1 week ($100) + 3 days ($150) = $250
|
|
// Round-up: 2 weeks ($200)
|
|
// Expected: $200 (round-up wins)
|
|
const item = { pricePerWeek: 100, pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("11", "10:00"), // 10 days
|
|
item
|
|
);
|
|
expect(result).toBe(200); // 2 weeks (cheaper)
|
|
});
|
|
});
|
|
|
|
describe("Hybrid month + weeks + days pricing", () => {
|
|
test("should combine month, week, and days for 38-day rental", () => {
|
|
const item = { pricePerMonth: 600, pricePerWeek: 200, pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-02-08T10:00:00.000Z", // 38 days
|
|
item
|
|
);
|
|
expect(result).toBe(850); // 1 month ($600) + 1 week ($200) + 1 day ($50)
|
|
});
|
|
|
|
test("should combine month and days for 35-day rental without weekly", () => {
|
|
const item = { pricePerMonth: 600, pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-02-05T10:00:00.000Z", // 35 days
|
|
item
|
|
);
|
|
expect(result).toBe(850); // 1 month ($600) + 5 days ($250)
|
|
});
|
|
});
|
|
|
|
describe("Full hybrid: month + week + day + hour", () => {
|
|
test("should combine all tiers for complex duration", () => {
|
|
const item = {
|
|
pricePerMonth: 600,
|
|
pricePerWeek: 200,
|
|
pricePerDay: 50,
|
|
pricePerHour: 10,
|
|
};
|
|
// 38 days + 5 hours
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-02-08T15:00:00.000Z",
|
|
item
|
|
);
|
|
// 1 month + 1 week + 1 day + 5 hours = $600 + $200 + $50 + $50 = $900
|
|
expect(result).toBe(900);
|
|
});
|
|
});
|
|
|
|
describe("Missing tier fallback", () => {
|
|
test("should round up to days when no hourly rate", () => {
|
|
const item = { pricePerDay: 50 }; // No hourly
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "15:00"),
|
|
date("02", "17:00"), // 26 hours
|
|
item
|
|
);
|
|
expect(result).toBe(100); // 2 days (rounded up)
|
|
});
|
|
|
|
test("should round up to weeks when no daily rate", () => {
|
|
const item = { pricePerWeek: 200 }; // No daily
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("10", "10:00"), // 9 days
|
|
item
|
|
);
|
|
expect(result).toBe(400); // 2 weeks (rounded up)
|
|
});
|
|
|
|
test("should round up to months when no weekly or daily rate", () => {
|
|
const item = { pricePerMonth: 600 }; // Only monthly
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-02-15T10:00:00.000Z", // 45 days
|
|
item
|
|
);
|
|
expect(result).toBe(1200); // 2 months (rounded up)
|
|
});
|
|
|
|
test("should use hourly only when no larger tiers", () => {
|
|
const item = { pricePerHour: 10 }; // Only hourly
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("03", "10:00"), // 48 hours
|
|
item
|
|
);
|
|
expect(result).toBe(480); // 48 hours * $10
|
|
});
|
|
});
|
|
|
|
describe("Edge cases", () => {
|
|
test("should return 0 for zero duration", () => {
|
|
const item = { pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("01", "10:00"),
|
|
item
|
|
);
|
|
expect(result).toBe(0);
|
|
});
|
|
|
|
test("should return 0 for negative duration", () => {
|
|
const item = { pricePerDay: 50 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("02", "10:00"),
|
|
date("01", "10:00"), // End before start
|
|
item
|
|
);
|
|
expect(result).toBe(0);
|
|
});
|
|
|
|
test("should return 0 for free items (no pricing)", () => {
|
|
const item = {}; // No pricing
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("05", "10:00"),
|
|
item
|
|
);
|
|
expect(result).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when all prices are 0", () => {
|
|
const item = {
|
|
pricePerHour: 0,
|
|
pricePerDay: 0,
|
|
pricePerWeek: 0,
|
|
pricePerMonth: 0,
|
|
};
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("05", "10:00"),
|
|
item
|
|
);
|
|
expect(result).toBe(0);
|
|
});
|
|
|
|
test("should handle decimal prices", () => {
|
|
const item = { pricePerHour: 5.5 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("01", "13:00"), // 3 hours
|
|
item
|
|
);
|
|
expect(result).toBe(16.5); // 3 * $5.50
|
|
});
|
|
|
|
test("should round result to 2 decimal places", () => {
|
|
const item = { pricePerHour: 3.33 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "10:00"),
|
|
date("01", "13:00"), // 3 hours
|
|
item
|
|
);
|
|
expect(result).toBe(9.99); // 3 * $3.33
|
|
});
|
|
|
|
test("should handle string prices in item", () => {
|
|
const item = { pricePerDay: "50", pricePerHour: "10" };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
date("01", "15:00"),
|
|
date("02", "17:00"), // 26 hours
|
|
item
|
|
);
|
|
expect(result).toBe(70); // 1 day + 2 hours
|
|
});
|
|
|
|
test("should handle very short rentals (less than 1 hour)", () => {
|
|
const item = { pricePerHour: 10 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-01-01T10:30:00.000Z", // 30 minutes
|
|
item
|
|
);
|
|
expect(result).toBe(10); // Rounds up to 1 hour
|
|
});
|
|
});
|
|
|
|
describe("Real-world scenarios", () => {
|
|
test("Scenario: Tool rental 3 days 4 hours", () => {
|
|
const item = { pricePerDay: 25, pricePerHour: 5 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T08:00:00.000Z",
|
|
"2024-01-04T12:00:00.000Z", // 3 days + 4 hours
|
|
item
|
|
);
|
|
expect(result).toBe(95); // 3 days ($75) + 4 hours ($20)
|
|
});
|
|
|
|
test("Scenario: Equipment rental 2 weeks 3 days", () => {
|
|
const item = { pricePerWeek: 100, pricePerDay: 20 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-01-18T10:00:00.000Z", // 17 days = 2 weeks + 3 days
|
|
item
|
|
);
|
|
expect(result).toBe(260); // 2 weeks ($200) + 3 days ($60)
|
|
});
|
|
|
|
test("Scenario: Long-term rental 2 months 1 week", () => {
|
|
const item = { pricePerMonth: 500, pricePerWeek: 150 };
|
|
const result = RentalDurationCalculator.calculateRentalCost(
|
|
"2024-01-01T10:00:00.000Z",
|
|
"2024-03-08T10:00:00.000Z", // ~67 days = 2 months + 1 week
|
|
item
|
|
);
|
|
expect(result).toBe(1150); // 2 months ($1000) + 1 week ($150)
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("buildPricingTiers", () => {
|
|
test("should return empty array for items with no pricing", () => {
|
|
const item = {};
|
|
const tiers = RentalDurationCalculator.buildPricingTiers(item);
|
|
expect(tiers).toHaveLength(0);
|
|
});
|
|
|
|
test("should build tiers from largest to smallest", () => {
|
|
const item = {
|
|
pricePerMonth: 600,
|
|
pricePerWeek: 200,
|
|
pricePerDay: 50,
|
|
pricePerHour: 10,
|
|
};
|
|
const tiers = RentalDurationCalculator.buildPricingTiers(item);
|
|
expect(tiers).toHaveLength(4);
|
|
expect(tiers[0].name).toBe("month");
|
|
expect(tiers[1].name).toBe("week");
|
|
expect(tiers[2].name).toBe("day");
|
|
expect(tiers[3].name).toBe("hour");
|
|
});
|
|
|
|
test("should skip tiers with 0 or null pricing", () => {
|
|
const item = {
|
|
pricePerMonth: 0,
|
|
pricePerWeek: null,
|
|
pricePerDay: 50,
|
|
pricePerHour: 10,
|
|
};
|
|
const tiers = RentalDurationCalculator.buildPricingTiers(item);
|
|
expect(tiers).toHaveLength(2);
|
|
expect(tiers[0].name).toBe("day");
|
|
expect(tiers[1].name).toBe("hour");
|
|
});
|
|
});
|
|
});
|