[
{ "name": "collected on the last free day: nothing to pay", "args": ["2026-03-01", "2026-03-04", 4, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "dailyRate": { "minor": 15000, "currency": "USD" } }, { "fromDay": 15, "toDay": null, "dailyRate": { "minor": 30000, "currency": "USD" } }]], "expect": { "totalDays": 4, "freeDaysUsed": 4, "chargeableDays": 0, "freeTimeEnds": "2026-03-04", "lines": [], "total": { "minor": 0, "currency": "USD" } } },
{ "name": "one day over free time", "args": ["2026-03-01", "2026-03-05", 4, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "dailyRate": { "minor": 15000, "currency": "USD" } }, { "fromDay": 15, "toDay": null, "dailyRate": { "minor": 30000, "currency": "USD" } }]], "expect": { "totalDays": 5, "freeDaysUsed": 4, "chargeableDays": 1, "freeTimeEnds": "2026-03-04", "lines": [{ "fromDay": 5, "toDay": 5, "days": 1, "dailyRate": { "minor": 7500, "currency": "USD" }, "amount": { "minor": 7500, "currency": "USD" } }], "total": { "minor": 7500, "currency": "USD" } } },
{ "name": "twenty days runs through all three tiers", "args": ["2026-03-01", "2026-03-20", 4, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "dailyRate": { "minor": 15000, "currency": "USD" } }, { "fromDay": 15, "toDay": null, "dailyRate": { "minor": 30000, "currency": "USD" } }]], "expect": { "totalDays": 20, "freeDaysUsed": 4, "chargeableDays": 16, "freeTimeEnds": "2026-03-04", "lines": [{ "fromDay": 5, "toDay": 7, "days": 3, "dailyRate": { "minor": 7500, "currency": "USD" }, "amount": { "minor": 22500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "days": 7, "dailyRate": { "minor": 15000, "currency": "USD" }, "amount": { "minor": 105000, "currency": "USD" } }, { "fromDay": 15, "toDay": 20, "days": 6, "dailyRate": { "minor": 30000, "currency": "USD" }, "amount": { "minor": 180000, "currency": "USD" } }], "total": { "minor": 307500, "currency": "USD" } } },
{ "name": "tiers given out of order give the same answer", "args": ["2026-03-01", "2026-03-20", 4, [{ "fromDay": 15, "toDay": null, "dailyRate": { "minor": 30000, "currency": "USD" } }, { "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "dailyRate": { "minor": 15000, "currency": "USD" } }]], "expect": { "totalDays": 20, "freeDaysUsed": 4, "chargeableDays": 16, "freeTimeEnds": "2026-03-04", "lines": [{ "fromDay": 5, "toDay": 7, "days": 3, "dailyRate": { "minor": 7500, "currency": "USD" }, "amount": { "minor": 22500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "days": 7, "dailyRate": { "minor": 15000, "currency": "USD" }, "amount": { "minor": 105000, "currency": "USD" } }, { "fromDay": 15, "toDay": 20, "days": 6, "dailyRate": { "minor": 30000, "currency": "USD" }, "amount": { "minor": 180000, "currency": "USD" } }], "total": { "minor": 307500, "currency": "USD" } } },
{ "name": "extended free time swallows the first tier and part of the second", "args": ["2026-03-01", "2026-03-20", 10, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": 14, "dailyRate": { "minor": 15000, "currency": "USD" } }, { "fromDay": 15, "toDay": null, "dailyRate": { "minor": 30000, "currency": "USD" } }]], "expect": { "totalDays": 20, "freeDaysUsed": 10, "chargeableDays": 10, "freeTimeEnds": "2026-03-10", "lines": [{ "fromDay": 11, "toDay": 14, "days": 4, "dailyRate": { "minor": 15000, "currency": "USD" }, "amount": { "minor": 60000, "currency": "USD" } }, { "fromDay": 15, "toDay": 20, "days": 6, "dailyRate": { "minor": 30000, "currency": "USD" }, "amount": { "minor": 180000, "currency": "USD" } }], "total": { "minor": 240000, "currency": "USD" } } },
{ "name": "free time runs through 29 February in a leap year", "args": ["2028-02-27", "2028-03-02", 4, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": 9000, "currency": "EUR" } }]], "expect": { "totalDays": 5, "freeDaysUsed": 4, "chargeableDays": 1, "freeTimeEnds": "2028-03-01", "lines": [{ "fromDay": 5, "toDay": 5, "days": 1, "dailyRate": { "minor": 9000, "currency": "EUR" }, "amount": { "minor": 9000, "currency": "EUR" } }], "total": { "minor": 9000, "currency": "EUR" } } },
{ "name": "returned early: free time still ends where it would have", "args": ["2026-12-30", "2026-12-31", 4, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": 9000, "currency": "EUR" } }]], "expect": { "totalDays": 2, "freeDaysUsed": 2, "chargeableDays": 0, "freeTimeEnds": "2027-01-02", "lines": [], "total": { "minor": 0, "currency": "EUR" } } },
{ "name": "no free time, a single day at a flat rate", "args": ["2026-06-15", "2026-06-15", 0, [{ "fromDay": 1, "toDay": null, "dailyRate": { "minor": 5000, "currency": "GBP" } }]], "expect": { "totalDays": 1, "freeDaysUsed": 0, "chargeableDays": 1, "freeTimeEnds": null, "lines": [{ "fromDay": 1, "toDay": 1, "days": 1, "dailyRate": { "minor": 5000, "currency": "GBP" }, "amount": { "minor": 5000, "currency": "GBP" } }], "total": { "minor": 5000, "currency": "GBP" } } },
{ "name": "a zero-rate tier charges nothing but still appears", "args": ["2026-06-01", "2026-06-10", 2, [{ "fromDay": 3, "toDay": 5, "dailyRate": { "minor": 0, "currency": "GBP" } }, { "fromDay": 6, "toDay": null, "dailyRate": { "minor": 4000, "currency": "GBP" } }]], "expect": { "totalDays": 10, "freeDaysUsed": 2, "chargeableDays": 8, "freeTimeEnds": "2026-06-02", "lines": [{ "fromDay": 3, "toDay": 5, "days": 3, "dailyRate": { "minor": 0, "currency": "GBP" }, "amount": { "minor": 0, "currency": "GBP" } }, { "fromDay": 6, "toDay": 10, "days": 5, "dailyRate": { "minor": 4000, "currency": "GBP" }, "amount": { "minor": 20000, "currency": "GBP" } }], "total": { "minor": 20000, "currency": "GBP" } } },
{ "name": "no free time with a tariff that starts at day 5 leaves days uncovered", "args": ["2026-03-01", "2026-03-06", 0, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "no rate for day 1" },
{ "name": "a gap in the tariff is an error, not a free day", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 9, "toDay": null, "dailyRate": { "minor": 15000, "currency": "USD" } }]], "expectError": "no rate for day 8" },
{ "name": "a closed last tier leaves later days uncovered", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "no rate for day 8" },
{ "name": "overlapping tiers", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 5, "toDay": 8, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": null, "dailyRate": { "minor": 15000, "currency": "USD" } }]], "expectError": "tiers overlap at day 8" },
{ "name": "an open tier followed by another overlaps", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 10, "toDay": null, "dailyRate": { "minor": 15000, "currency": "USD" } }]], "expectError": "tiers overlap at day 10" },
{ "name": "a tier running backwards", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 7, "toDay": 5, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "tier toDay must not be before fromDay" },
{ "name": "a tier starting at day 0", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 0, "toDay": null, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "tier fromDay must be 1 or more" },
{ "name": "a negative daily rate", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": -7500, "currency": "USD" } }]], "expectError": "dailyRate must not be negative" },
{ "name": "tiers in two currencies", "args": ["2026-03-01", "2026-03-10", 4, [{ "fromDay": 5, "toDay": 7, "dailyRate": { "minor": 7500, "currency": "USD" } }, { "fromDay": 8, "toDay": null, "dailyRate": { "minor": 15000, "currency": "EUR" } }]], "expectError": "currency mismatch" },
{ "name": "no tiers at all", "args": ["2026-03-01", "2026-03-02", 4, []], "expectError": "tiers must not be empty" },
{ "name": "negative free days", "args": ["2026-03-01", "2026-03-10", -1, [{ "fromDay": 1, "toDay": null, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "freeDays must not be negative" },
{ "name": "end before start", "args": ["2026-03-10", "2026-03-09", 4, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "endDate must not be before startDate" },
{ "name": "an impossible date", "args": ["2026-02-30", "2026-03-09", 4, [{ "fromDay": 5, "toDay": null, "dailyRate": { "minor": 7500, "currency": "USD" } }]], "expectError": "2026-02-30" }
]