[ { "name": "an April-March CPD year on 15 March: the first day counts, the day before and a booked course after today do not", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2026-03-15" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 840, "entriesCounted": 3, "requiredMinutes": 2100, "shortfallMinutes": 1260, "met": false, "daysRemaining": 16 } }, { "name": "the requirement met", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 800, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2026-03-15" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 840, "entriesCounted": 3, "requiredMinutes": 800, "shortfallMinutes": 0, "met": true, "daysRemaining": 16 } }, { "name": "exactly met", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 840, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2026-03-15" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 840, "entriesCounted": 3, "requiredMinutes": 840, "shortfallMinutes": 0, "met": true, "daysRemaining": 16 } }, { "name": "the new CPD year starts from nothing", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2026-04-05" ], "expect": { "periodStart": "2026-04-01", "periodEnd": "2027-03-31", "loggedMinutes": 0, "entriesCounted": 0, "requiredMinutes": 2100, "shortfallMinutes": 2100, "met": false, "daysRemaining": 360 } }, { "name": "the last day of a fixed year", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2026-03-31" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 930, "entriesCounted": 4, "requiredMinutes": 2100, "shortfallMinutes": 1170, "met": false, "daysRemaining": 0 } }, { "name": "an anchor in the future still finds the period, counting back", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2027-04-01" }, "2026-03-15" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 840, "entriesCounted": 3, "requiredMinutes": 2100, "shortfallMinutes": 1260, "met": false, "daysRemaining": 16 } }, { "name": "a rolling year on 5 April 2026 runs from 6 April 2025", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": true, "anchor": null }, "2026-04-05" ], "expect": { "periodStart": "2025-04-06", "periodEnd": "2026-04-05", "loggedMinutes": 750, "entriesCounted": 3, "requiredMinutes": 2100, "shortfallMinutes": 1350, "met": false, "daysRemaining": 0 } }, { "name": "a rolling three years, as for revalidation", "args": [ [ { "date": "2026-03-20", "minutes": 90 }, { "date": "2025-03-31", "minutes": 120 }, { "date": "2025-04-01", "minutes": 180 }, { "date": "2025-10-10", "minutes": 600 }, { "date": "2026-03-15", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 36, "rolling": true, "anchor": null }, "2026-03-15" ], "expect": { "periodStart": "2023-03-16", "periodEnd": "2026-03-15", "loggedMinutes": 960, "entriesCounted": 4, "requiredMinutes": 2100, "shortfallMinutes": 1140, "met": false, "daysRemaining": 0 } }, { "name": "a rolling year ending on 29 February starts on 1 March", "args": [ [ { "date": "2023-02-28", "minutes": 60 }, { "date": "2023-03-01", "minutes": 30 }, { "date": "2024-02-29", "minutes": 45 } ], { "requiredMinutes": 100, "periodMonths": 12, "rolling": true, "anchor": null }, "2024-02-29" ], "expect": { "periodStart": "2023-03-01", "periodEnd": "2024-02-29", "loggedMinutes": 75, "entriesCounted": 2, "requiredMinutes": 100, "shortfallMinutes": 25, "met": false, "daysRemaining": 0 } }, { "name": "monthly periods anchored on the 31st: February's starts on the 28th, March's on the 31st again", "args": [ [ { "date": "2025-03-30", "minutes": 60 }, { "date": "2025-03-31", "minutes": 30 } ], { "requiredMinutes": 120, "periodMonths": 1, "rolling": false, "anchor": "2025-01-31" }, "2025-03-15" ], "expect": { "periodStart": "2025-02-28", "periodEnd": "2025-03-30", "loggedMinutes": 0, "entriesCounted": 0, "requiredMinutes": 120, "shortfallMinutes": 120, "met": false, "daysRemaining": 15 } }, { "name": "an empty log", "args": [ [], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2025-06-01" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 0, "entriesCounted": 0, "requiredMinutes": 2100, "shortfallMinutes": 2100, "met": false, "daysRemaining": 303 } }, { "name": "no requirement is always met", "args": [ [], { "requiredMinutes": 0, "periodMonths": 12, "rolling": true, "anchor": null }, "2025-06-01" ], "expect": { "periodStart": "2024-06-02", "periodEnd": "2025-06-01", "loggedMinutes": 0, "entriesCounted": 0, "requiredMinutes": 0, "shortfallMinutes": 0, "met": true, "daysRemaining": 0 } }, { "name": "a zero-minute entry counts as an entry and adds nothing", "args": [ [ { "date": "2025-05-01", "minutes": 0 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2025-06-01" ], "expect": { "periodStart": "2025-04-01", "periodEnd": "2026-03-31", "loggedMinutes": 0, "entriesCounted": 1, "requiredMinutes": 2100, "shortfallMinutes": 2100, "met": false, "daysRemaining": 303 } }, { "name": "negative minutes are an error", "args": [ [ { "date": "2025-05-01", "minutes": -30 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2025-06-01" ], "expectError": "minutes must be a whole number of 0 or more, received -30 on 2025-05-01" }, { "name": "fractional minutes are an error", "args": [ [ { "date": "2025-05-01", "minutes": 1.5 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2025-06-01" ], "expectError": "minutes must be a whole number of 0 or more, received 1.5" }, { "name": "a negative requirement is an error", "args": [ [], { "requiredMinutes": -1, "periodMonths": 12, "rolling": true, "anchor": null }, "2025-06-01" ], "expectError": "requiredMinutes must be a whole number of 0 or more, received -1" }, { "name": "a zero-month period is an error", "args": [ [], { "requiredMinutes": 60, "periodMonths": 0, "rolling": true, "anchor": null }, "2025-06-01" ], "expectError": "periodMonths must be a whole number of 1 or more, received 0" }, { "name": "a fixed period without an anchor is an error", "args": [ [], { "requiredMinutes": 60, "periodMonths": 12, "rolling": false, "anchor": null }, "2025-06-01" ], "expectError": "a fixed period needs an anchor date" }, { "name": "a rolling period with an anchor is an error", "args": [ [], { "requiredMinutes": 60, "periodMonths": 12, "rolling": true, "anchor": "2025-04-01" }, "2025-06-01" ], "expectError": "a rolling period takes no anchor" }, { "name": "an impossible entry date is an error", "args": [ [ { "date": "2025-02-30", "minutes": 60 } ], { "requiredMinutes": 2100, "periodMonths": 12, "rolling": false, "anchor": "2025-04-01" }, "2025-06-01" ], "expectError": "\"2025-02-30\" is not a real calendar date" } ]