feat(medications): make low-stock alert reorder-lead-time aware

The low-stock alert fired when the projected runway dropped below the
user threshold (default 7 days). For a sparse cadence — a weekly
injection's 7-day floor is barely one dose-interval — the warning landed
with about one dose left, too late to reorder before the supply ran out.

Add an optional per-medication reorder lead time plus a user-level
default (0–60 days, default 10) and widen the alert trigger to
max(lowStockRunwayDays, leadDays + cadenceIntervalDays), so the warning
lands before the last dose for any cadence. Because the trigger is a
max() over the user floor it never shrinks anyone's current threshold; a
daily med with no lead keeps its prior boundary.

The push body and the card notice surface concrete dates — supply runs
out around <date>, reorder by <date> — with a last-dose informational
state and the days/units fallback when supply data is thin. Retire the
stale fixed 4-dose count threshold on the GLP-1 details endpoint and
route its card lowStock flag through the same runway evaluation so the
card and the daily cron agree. Surface the lead default in the low-stock
settings card with progressive disclosure; a single medication can
override it.

Additive schema: nullable Medication.reorderLeadDays (migration 0161, no
backfill) and notificationPrefs.medication.reorderLeadDays. OpenAPI
regenerated.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 20:39:06 +02:00
1 parent 52bf985734
commit 08aa655be4
29 files changed
+1060 -97

No files matched your search

@@ -77,12 +77,15 @@ describe("GET /api/settings/reminder-thresholds", () => {
lateMinutes: number;
missedMinutes: number;
lowStockRunwayDays: number | null;
reorderLeadDays: number;
};
};
expect(env.data).toEqual({
lateMinutes: 120,
missedMinutes: 240,
lowStockRunwayDays: 7,
// v1.17.0 — the per-user reorder lead default rides along.
reorderLeadDays: 10,
});
});
@@ -3,7 +3,7 @@ import { getReminderThresholds } from "@/lib/app-settings";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { resolveLowStockRunwayDays } from "@/lib/validations/notification-prefs";
import { parseNotificationPrefs } from "@/lib/validations/notification-prefs";
export const dynamic = "force-dynamic";
@@ -22,11 +22,15 @@ export const GET = apiHandler(async () => {
where: { id: user.id },
select: { notificationPrefs: true },
});
const lowStockRunwayDays = resolveLowStockRunwayDays(
row?.notificationPrefs ?? null,
);
const prefs = parseNotificationPrefs(row?.notificationPrefs ?? null);
const lowStockRunwayDays = prefs.medication.lowStockRunwayDays;
// v1.17.0 — the user-level reorder lead default rides along so the
// medication cards can derive the same reorder-lead-aware trigger the
// daily cron uses (per-medication overrides come from the list payload's
// own `reorderLeadDays`).
const reorderLeadDays = prefs.medication.reorderLeadDays;
annotate({ action: { name: "settings.reminder-thresholds.get" } });
return apiSuccess({ ...thresholds, lowStockRunwayDays });
return apiSuccess({ ...thresholds, lowStockRunwayDays, reorderLeadDays });
});