fix: backend planner phantom consumption + PUT stock reset (#115)

Two bugs in the backend medications route:

1. Planner /medications/usage had the same +1 phantom consumption bug
   that was fixed in the frontend (PR #109). After a stock correction,
   effectiveStart was set to max(blisterStart, correctionCutoff) instead
   of correctionCutoff + period, causing 1 dose to be immediately
   counted as consumed.

2. PUT /medications/:id did not reset stockAdjustment when stock fields
   (packCount, blistersPerPack, pillsPerBlister, looseTablets) changed.
   If a user edited stock values to correct their inventory, the old
   stockAdjustment offset was preserved, resulting in wrong totals.

Added 4 tests covering both scenarios.
This commit is contained in:
Daniel Volz
2026-02-08 11:05:56 +01:00
committed by GitHub
parent 6b3a7b4104
commit 99bb9c3931
2 changed files with 166 additions and 2 deletions
+143
View File
@@ -1883,6 +1883,149 @@ describe("E2E Tests with Real Routes", () => {
expect(response.statusCode).toBe(400);
});
it("should reset stockAdjustment when stock fields change via PUT", async () => {
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload: {
name: "Reset Adj Med",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 30,
looseTablets: 0,
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
});
const medId = createResponse.json().id;
// Set stock adjustment to -10
await app.inject({
method: "PATCH",
url: `/medications/${medId}/stock-adjustment`,
payload: { stockAdjustment: -10 },
});
// Verify adjustment is set
let getMeds = await app.inject({ method: "GET", url: "/medications" });
let med = getMeds.json().find((m: any) => m.id === medId);
expect(med.stockAdjustment).toBe(-10);
// Edit medication with CHANGED stock fields (packCount 1 → 2)
await app.inject({
method: "PUT",
url: `/medications/${medId}`,
payload: {
name: "Reset Adj Med",
packCount: 2,
blistersPerPack: 1,
pillsPerBlister: 30,
looseTablets: 0,
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
});
// stockAdjustment should be reset to 0
getMeds = await app.inject({ method: "GET", url: "/medications" });
med = getMeds.json().find((m: any) => m.id === medId);
expect(med.stockAdjustment).toBe(0);
expect(med.lastStockCorrectionAt).toBeTruthy();
});
it("should preserve stockAdjustment when only non-stock fields change via PUT", async () => {
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload: {
name: "Preserve Adj Med",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 30,
looseTablets: 0,
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
});
const medId = createResponse.json().id;
// Set stock adjustment
await app.inject({
method: "PATCH",
url: `/medications/${medId}/stock-adjustment`,
payload: { stockAdjustment: -5 },
});
// Edit only non-stock fields (name, notes)
await app.inject({
method: "PUT",
url: `/medications/${medId}`,
payload: {
name: "Renamed Preserve Med",
notes: "Updated notes",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 30,
looseTablets: 0,
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
});
// stockAdjustment should be preserved
const getMeds = await app.inject({ method: "GET", url: "/medications" });
const med = getMeds.json().find((m: any) => m.id === medId);
expect(med.name).toBe("Renamed Preserve Med");
expect(med.stockAdjustment).toBe(-5);
});
it("should not count phantom consumption in planner after stock correction", async () => {
// Create medication: 1 pack × 14 blisters × 14 pills = 196 pills total
// Schedule: 1 pill daily starting far in the past
const farPast = new Date("2024-01-01T08:00:00.000Z");
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload: {
name: "Planner Phantom Med",
packCount: 1,
blistersPerPack: 14,
pillsPerBlister: 14,
looseTablets: 0,
blisters: [{ usage: 1, every: 1, start: farPast.toISOString() }],
},
});
const medId = createResponse.json().id;
// Correct stock to 113 pills (196 base - 83 = 113)
await app.inject({
method: "PATCH",
url: `/medications/${medId}/stock-adjustment`,
payload: { stockAdjustment: -83 },
});
// Query planner immediately - stock should be ~113 (not reduced by phantom dose)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);
const response = await app.inject({
method: "POST",
url: "/medications/usage",
payload: {
startDate: tomorrow.toISOString(),
endDate: nextWeek.toISOString(),
},
});
expect(response.statusCode).toBe(200);
const data = response.json();
const med = data.find((m: any) => m.medicationId === medId);
expect(med).toBeDefined();
// Total should be very close to 113 (not 112 or lower from phantom consumption)
// Allow up to 1 pill of natural consumption (test runs fast, but at most 1 day could pass)
expect(med.totalPills).toBeGreaterThanOrEqual(112);
expect(med.totalPills).toBeLessThanOrEqual(113);
});
});
// ---------------------------------------------------------------------------