@@ -942,17 +942,17 @@ describe("Integration Tests", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Planner usage calculation", () => {
|
||||
const plannerWindowStart = "2030-01-15T00:00:00.000Z";
|
||||
const futureDailyStart = "2030-01-15T08:00:00.000Z";
|
||||
const futureEveningStart = "2030-01-15T20:00:00.000Z";
|
||||
const tenDayPlanEnd = "2030-01-24T23:59:59.999Z";
|
||||
const thirtyFiveDayPlanEnd = "2030-02-18T23:59:59.999Z";
|
||||
|
||||
it("should calculate correct usage for daily medication", async () => {
|
||||
// Create medication: 2 packs × 3 blisters × 10 pills = 60 pills total
|
||||
// Schedule: 1 pill daily starting tomorrow (future date)
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
// Schedule: 1 pill daily starting on a fixed future winter date.
|
||||
// This avoids daylight-saving-time edge cases in local test environments.
|
||||
const intakeStart = futureDailyStart;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
@@ -972,8 +972,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr, // 10 days
|
||||
startDate: plannerWindowStart,
|
||||
endDate: tenDayPlanEnd,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -988,15 +988,8 @@ describe("Integration Tests", () => {
|
||||
|
||||
it("should detect insufficient stock", async () => {
|
||||
// Create medication: 1 pack × 1 blister × 5 pills = 5 pills total
|
||||
// Schedule: 1 pill daily starting tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
// Schedule: 1 pill daily starting on a fixed future winter date.
|
||||
const intakeStart = futureDailyStart;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
@@ -1016,8 +1009,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr,
|
||||
startDate: plannerWindowStart,
|
||||
endDate: tenDayPlanEnd,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1029,15 +1022,8 @@ describe("Integration Tests", () => {
|
||||
|
||||
it("should calculate weekly medication usage correctly", async () => {
|
||||
// Create medication: 10 pills total
|
||||
// Schedule: 1 pill every 7 days starting tomorrow
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 35); // 35 days to get 5 weekly doses
|
||||
const planEndStr = planEnd.toISOString();
|
||||
// Schedule: 1 pill every 7 days starting on a fixed future winter date.
|
||||
const intakeStart = futureDailyStart;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
@@ -1056,8 +1042,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr,
|
||||
startDate: plannerWindowStart,
|
||||
endDate: thirtyFiveDayPlanEnd,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1070,18 +1056,8 @@ describe("Integration Tests", () => {
|
||||
it("should handle multiple intake schedules per medication", async () => {
|
||||
// Create medication with morning and evening doses
|
||||
// 30 pills total, 1.5 pills per day (1 morning + 0.5 evening)
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const morningStart = tomorrow.toISOString();
|
||||
|
||||
const eveningStart = new Date(tomorrow);
|
||||
eveningStart.setHours(20, 0, 0, 0);
|
||||
const eveningStartStr = eveningStart.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
const morningStart = futureDailyStart;
|
||||
const eveningStartStr = futureEveningStart;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
@@ -1103,8 +1079,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: morningStart,
|
||||
endDate: planEndStr,
|
||||
startDate: plannerWindowStart,
|
||||
endDate: tenDayPlanEnd,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1116,14 +1092,7 @@ describe("Integration Tests", () => {
|
||||
|
||||
it("should calculate correct blisters needed", async () => {
|
||||
// 10 pills per blister, need 25 pills → need 3 blisters
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(8, 0, 0, 0);
|
||||
const intakeStart = tomorrow.toISOString();
|
||||
|
||||
const planEnd = new Date(tomorrow);
|
||||
planEnd.setDate(planEnd.getDate() + 10);
|
||||
const planEndStr = planEnd.toISOString();
|
||||
const intakeStart = futureDailyStart;
|
||||
|
||||
await app.inject({
|
||||
method: "POST",
|
||||
@@ -1142,8 +1111,8 @@ describe("Integration Tests", () => {
|
||||
method: "POST",
|
||||
url: "/medications/usage",
|
||||
payload: {
|
||||
startDate: intakeStart,
|
||||
endDate: planEndStr,
|
||||
startDate: plannerWindowStart,
|
||||
endDate: tenDayPlanEnd,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,22 +6,30 @@ import {
|
||||
calculateDailyUsage,
|
||||
calculateDepletionInfo,
|
||||
cleanOldIntakeReminders,
|
||||
countScheduledOccurrencesInRange,
|
||||
createDefaultIntakeReminderState,
|
||||
createDefaultReminderState,
|
||||
forEachScheduledOccurrenceInRange,
|
||||
formatInTimezone,
|
||||
getAverageOccurrencesPerDay,
|
||||
getCurrentHourInTimezone,
|
||||
getMaxScheduledGapDays,
|
||||
getMsUntilNextCheck,
|
||||
getNextScheduledOccurrenceTime,
|
||||
getNextScheduledTime,
|
||||
getTimezone,
|
||||
getTodayInTimezone,
|
||||
getTodaysIntakes,
|
||||
getUpcomingIntakes,
|
||||
type Intake,
|
||||
normalizeIntake,
|
||||
parseBlisters,
|
||||
parseIntakeReminderState,
|
||||
parseIntakesJson,
|
||||
parseReminderState,
|
||||
parseTakenByJson,
|
||||
personTakesMedication,
|
||||
type Weekday,
|
||||
} from "../utils/scheduler-utils.js";
|
||||
|
||||
// Helper to convert Blister to Intake for tests
|
||||
@@ -267,6 +275,77 @@ describe("Scheduler Utils - Blister Parsing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scheduler Utils - Intake Schedule Normalization", () => {
|
||||
describe("normalizeIntake", () => {
|
||||
it("keeps interval schedules backward-compatible by default", () => {
|
||||
const intake = normalizeIntake({
|
||||
usage: 2,
|
||||
every: 3,
|
||||
start: "2025-01-01T08:00:00",
|
||||
});
|
||||
|
||||
expect(intake).toMatchObject({
|
||||
usage: 2,
|
||||
every: 3,
|
||||
start: "2025-01-01T08:00:00",
|
||||
scheduleMode: "interval",
|
||||
weekdays: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes malformed weekday schedules to the start date weekday", () => {
|
||||
const intake = normalizeIntake({
|
||||
usage: 1,
|
||||
every: 99,
|
||||
start: "2025-01-06T08:00:00",
|
||||
scheduleMode: "weekdays",
|
||||
weekdays: ["bogus", null],
|
||||
});
|
||||
|
||||
expect(intake.scheduleMode).toBe("weekdays");
|
||||
expect(intake.every).toBe(1);
|
||||
expect(intake.weekdays).toEqual(["mon"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseIntakesJson", () => {
|
||||
it("falls back to legacy interval data when unified intakes are absent", () => {
|
||||
const intakes = parseIntakesJson(
|
||||
null,
|
||||
{
|
||||
usageJson: "[1,2]",
|
||||
everyJson: "[1,3]",
|
||||
startJson: '["2025-01-01T08:00:00","2025-01-02T20:00:00"]',
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
expect(intakes).toEqual([
|
||||
{
|
||||
usage: 1,
|
||||
every: 1,
|
||||
start: "2025-01-01T08:00:00",
|
||||
scheduleMode: "interval",
|
||||
weekdays: [],
|
||||
intakeUnit: null,
|
||||
takenBy: null,
|
||||
intakeRemindersEnabled: true,
|
||||
},
|
||||
{
|
||||
usage: 2,
|
||||
every: 3,
|
||||
start: "2025-01-02T20:00:00",
|
||||
scheduleMode: "interval",
|
||||
weekdays: [],
|
||||
intakeUnit: null,
|
||||
takenBy: null,
|
||||
intakeRemindersEnabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scheduler Utils - Daily Usage Calculation", () => {
|
||||
describe("calculateDailyUsage", () => {
|
||||
it("should calculate daily usage for single daily dose", () => {
|
||||
@@ -306,6 +385,71 @@ describe("Scheduler Utils - Daily Usage Calculation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scheduler Utils - Schedule Occurrence Calculation", () => {
|
||||
it("calculates average usage and gap length for weekday schedules", () => {
|
||||
const weekdaysSchedule = {
|
||||
every: 1,
|
||||
start: "2025-01-06T09:00:00",
|
||||
scheduleMode: "weekdays" as const,
|
||||
weekdays: ["mon", "wed", "fri"] satisfies Weekday[],
|
||||
};
|
||||
|
||||
expect(getAverageOccurrencesPerDay(weekdaysSchedule)).toBeCloseTo(3 / 7, 5);
|
||||
expect(getMaxScheduledGapDays(weekdaysSchedule)).toBe(3);
|
||||
expect(getAverageOccurrencesPerDay({ every: 2, start: "2025-01-01T09:00:00" })).toBe(0.5);
|
||||
expect(getMaxScheduledGapDays({ every: 2, start: "2025-01-01T09:00:00" })).toBe(2);
|
||||
});
|
||||
|
||||
it("finds the next weekday occurrence after a given timestamp", () => {
|
||||
const schedule = {
|
||||
every: 1,
|
||||
start: "2025-01-06T09:00:00",
|
||||
scheduleMode: "weekdays" as const,
|
||||
weekdays: ["mon", "wed", "fri"] satisfies Weekday[],
|
||||
};
|
||||
|
||||
const fromMs = new Date(2025, 0, 7, 12, 0, 0).getTime();
|
||||
const nextOccurrence = getNextScheduledOccurrenceTime(schedule, fromMs);
|
||||
|
||||
expect(nextOccurrence).toBe(new Date(2025, 0, 8, 9, 0, 0).getTime());
|
||||
});
|
||||
|
||||
it("iterates weekday occurrences in canonical order within a range", () => {
|
||||
const schedule = {
|
||||
every: 1,
|
||||
start: "2025-01-06T09:00:00",
|
||||
scheduleMode: "weekdays" as const,
|
||||
weekdays: ["wed", "mon", "fri"] satisfies Weekday[],
|
||||
};
|
||||
const occurrences: number[] = [];
|
||||
|
||||
forEachScheduledOccurrenceInRange(
|
||||
schedule,
|
||||
new Date(2025, 0, 6, 0, 0, 0).getTime(),
|
||||
new Date(2025, 0, 12, 23, 59, 59).getTime(),
|
||||
(occurrenceMs) => {
|
||||
occurrences.push(occurrenceMs);
|
||||
}
|
||||
);
|
||||
|
||||
expect(occurrences.sort((a, b) => a - b)).toEqual([
|
||||
new Date(2025, 0, 6, 9, 0, 0).getTime(),
|
||||
new Date(2025, 0, 8, 9, 0, 0).getTime(),
|
||||
new Date(2025, 0, 10, 9, 0, 0).getTime(),
|
||||
]);
|
||||
expect(
|
||||
countScheduledOccurrencesInRange(
|
||||
schedule,
|
||||
new Date(2025, 0, 6, 0, 0, 0).getTime(),
|
||||
new Date(2025, 0, 12, 23, 59, 59).getTime()
|
||||
)
|
||||
).toEqual({
|
||||
count: 3,
|
||||
lastOccurrenceMs: new Date(2025, 0, 10, 9, 0, 0).getTime(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scheduler Utils - Depletion Calculation", () => {
|
||||
describe("calculateDepletionInfo", () => {
|
||||
it("should calculate days left correctly", () => {
|
||||
@@ -378,12 +522,17 @@ describe("Scheduler Utils - Upcoming Intakes", () => {
|
||||
expect(result[0].pillWeightMg).toBe(500);
|
||||
});
|
||||
|
||||
it("should skip blisters with zero interval", () => {
|
||||
it("should treat zero interval as a daily fallback", () => {
|
||||
const intakes: Intake[] = [blisterToIntake({ usage: 1, every: 0, start: "2025-01-01T08:00:00" })];
|
||||
const now = new Date(2025, 0, 1, 7, 45, 0).getTime();
|
||||
|
||||
const result = getUpcomingIntakes("TestMed", intakes, 15, [], null, "en-US", "UTC", now);
|
||||
expect(result).toEqual([]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
medName: "TestMed",
|
||||
usage: 1,
|
||||
takenBy: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiple blisters", () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
createTestMedication,
|
||||
createTestShareToken,
|
||||
createTestUser,
|
||||
setUserSettings,
|
||||
type TestContext,
|
||||
} from "./setup.js";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user