fix(medications): size weekly-cadence supply runway correctly

`estimateDailyDoseCount` only special-cased FREQ=MONTHLY and FREQ=YEARLY.
A modern FREQ=WEEKLY rrule (the encoding the create path stores, with an
empty daysOfWeek) fell through to the legacy branch at seven days per week,
so a once-weekly injection counted as one dose per day. The detail card's
"lasts about N days" and the daily low-stock job — which share this math —
fired reorder alerts roughly seven times too early (fourteen for bi-weekly).

Parse FREQ=WEEKLY honouring INTERVAL and BYDAY count:
perDay += (times * bydayCount) / (7 * interval).
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 03:28:02 +02:00
1 parent d85ac3e555
commit 3405bb6fd3
2 files changed
+56

No files matched your search

@@ -68,6 +68,43 @@ describe("estimateDailyDoseCount", () => {
).toBeCloseTo(1 / 30);
});
it("treats a once-weekly RRULE as one dose per 7 days", () => {
// FREQ=WEEKLY;BYDAY=MO is the modern once-weekly injection encoding;
// it must NOT fall through to the daysPerWeek=7 legacy branch.
expect(
estimateDailyDoseCount([
schedule({ timesOfDay: ["08:00"], rrule: "FREQ=WEEKLY;BYDAY=MO" }),
]),
).toBeCloseTo(1 / 7);
});
it("halves a bi-weekly RRULE via INTERVAL=2 (one dose per 14 days)", () => {
expect(
estimateDailyDoseCount([
schedule({
timesOfDay: ["08:00"],
rrule: "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO",
}),
]),
).toBeCloseTo(1 / 14);
});
it("counts each BYDAY pick on a weekly RRULE (Mon+Thu → 2 per 7 days)", () => {
expect(
estimateDailyDoseCount([
schedule({ timesOfDay: ["08:00"], rrule: "FREQ=WEEKLY;BYDAY=MO,TH" }),
]),
).toBeCloseTo(2 / 7);
});
it("defaults a weekly RRULE with no BYDAY to one pick per week", () => {
expect(
estimateDailyDoseCount([
schedule({ timesOfDay: ["08:00"], rrule: "FREQ=WEEKLY" }),
]),
).toBeCloseTo(1 / 7);
});
it("sums across multiple schedules", () => {
expect(
estimateDailyDoseCount([
@@ -46,6 +46,25 @@ export function estimateDailyDoseCount(schedules: RunwaySchedule[]): number {
perDay += times / 365;
continue;
}
// FREQ=WEEKLY;BYDAY=…;INTERVAL=… is the modern weekly encoding (the
// create path stores the cadence on the rrule and leaves daysOfWeek
// empty). Honour the BYDAY pick count and the week INTERVAL: a
// once-weekly injection is one dose per 7 days, a bi-weekly one per
// 14. Without this branch a weekly rrule fell through to the legacy
// daysOfWeek fallback below with daysPerWeek=7, over-estimating the
// rate ~7× (≈14× bi-weekly) and firing low-stock alerts far too early.
if (/FREQ=WEEKLY/.test(rrule)) {
const byday = /BYDAY=([^;]+)/.exec(rrule);
const bydayCount =
byday && byday[1].length > 0 ? byday[1].split(",").length : 1;
const intervalMatch = /INTERVAL=(\d+)/.exec(rrule);
const interval =
intervalMatch && Number(intervalMatch[1]) >= 1
? Number(intervalMatch[1])
: 1;
perDay += (times * bydayCount) / (7 * interval);
continue;
}
const { daysOfWeek, intervalWeeks } = parseScheduleRecurrence(s.daysOfWeek);
const daysPerWeek = daysOfWeek.length > 0 ? daysOfWeek.length : 7;
const weeks = intervalWeeks >= 1 ? intervalWeeks : 1;