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"); }); }); });