fix: stock correction not working for bottle type and manual calculation mode (#133)
- Fix bottle type: submitStockCorrection used blister formula for baseTotal but getMedTotal uses only looseTablets for bottles. Now uses getPackageSize() which handles both types correctly. - Fix manual mode: same-day taken doses were counted as consumed after a stock correction (>= comparison with date-only timestamps). Changed to > so doses on the correction day are excluded. - Add agent instruction: only release-manager may create PRs/push/merge.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
- **English is the primary language**: All code, comments, documentation, commit messages, PR descriptions, and GitHub releases MUST be written in English. The user may communicate in German, but all project artifacts must be in English.
|
||||
- **NEVER release without explicit permission**: Do NOT create tags, releases, or version bumps unless the user explicitly asks for it. Always wait for explicit confirmation before any release action.
|
||||
- **NEVER create PRs without explicit permission**: Do NOT create Pull Requests, push branches, or merge code unless the user explicitly asks for it. Always present changes and wait for the user to confirm before any git operations that affect the remote repository.
|
||||
- **NEVER create PRs, push, or merge**: Only the **release-manager agent** (`@release-manager`) is allowed to create Pull Requests, push branches to the remote, or merge code. Regular agents and Copilot MUST NOT perform any git operations that affect the remote repository (no `git push`, no `gh pr create`, no `gh pr merge`). Present your local changes and tell the user to invoke `@release-manager` when ready to ship.
|
||||
- **No temporary files**: Delete temporary scripts/files immediately after use. Do not commit temporary debug scripts, test files, or one-off utilities to the repository.
|
||||
- **Clean workspace**: Always clean up after yourself. If you create a file for a specific task, delete it once done.
|
||||
- **Remove old code when re-implementing**: When fixing a bug or re-implementing a feature that didn't work, ALWAYS remove the old/broken code completely. Never leave dead code, unused functions, or obsolete implementations in the codebase.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { Coverage, FormState, Medication, RefillEntry } from "../types";
|
||||
import { getMedTotal } from "../types";
|
||||
import { getMedTotal, getPackageSize } from "../types";
|
||||
|
||||
export interface UseRefillReturn {
|
||||
// Refill state
|
||||
@@ -146,8 +146,8 @@ export function useRefill(): UseRefillReturn {
|
||||
const desiredTotal = finalFullBlisters * selectedMed.pillsPerBlister + finalPartialPills;
|
||||
|
||||
// The "base" from DB structure (without any stockAdjustment)
|
||||
const baseTotal =
|
||||
selectedMed.packCount * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + selectedMed.looseTablets;
|
||||
// Use getPackageSize() which handles both blister and bottle types correctly
|
||||
const baseTotal = getPackageSize(selectedMed);
|
||||
|
||||
// stockAdjustment = what we need to make getMedTotal() return desiredTotal
|
||||
const newStockAdjustment = desiredTotal - baseTotal;
|
||||
|
||||
@@ -287,6 +287,114 @@ describe("useRefill", () => {
|
||||
expect(mockLoadMeds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stock correction uses correct base for bottle type medications", async () => {
|
||||
// BUG FIX: submitStockCorrection used blister formula (packCount * blistersPerPack * pillsPerBlister + looseTablets)
|
||||
// for ALL medications, but getMedTotal() uses only looseTablets + stockAdjustment for bottles.
|
||||
// This mismatch caused the correction to compute the wrong stockAdjustment.
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
|
||||
|
||||
const bottleMed: Medication = {
|
||||
id: 4,
|
||||
name: "Pills in a Box",
|
||||
packageType: "bottle",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 1,
|
||||
looseTablets: 150,
|
||||
stockAdjustment: -2,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2026-01-31T20:27:00" }],
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
// getMedTotal for bottle = looseTablets + stockAdjustment = 150 + (-2) = 148
|
||||
// getPackageSize for bottle = looseTablets = 150
|
||||
|
||||
const mockLoadMeds = vi.fn();
|
||||
const { result } = renderHook(() => useRefill());
|
||||
|
||||
// Pre-fill: user sees 148 pills (148 / 1 = 148 full, 0 partial)
|
||||
act(() => {
|
||||
result.current.openEditStockModal(bottleMed, {
|
||||
all: [{ name: "Pills in a Box", medsLeft: 148, daysLeft: 148 }] as Coverage[],
|
||||
});
|
||||
});
|
||||
|
||||
// User adds +1 → 149 full blisters (pillsPerBlister=1)
|
||||
act(() => {
|
||||
result.current.setEditStockFullBlisters(149);
|
||||
result.current.setEditStockPartialBlisterPills(0);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitStockCorrection(4, bottleMed, mockLoadMeds);
|
||||
});
|
||||
|
||||
// desiredTotal = 149 * 1 + 0 = 149
|
||||
// baseTotal (fixed) = getPackageSize(bottle) = looseTablets = 150
|
||||
// newStockAdjustment = 149 - 150 = -1
|
||||
// → getMedTotal = 150 + (-1) = 149 ✓
|
||||
const fetchCall = (global.fetch as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
(call: [string, RequestInit]) => call[0] === "/api/medications/4/stock-adjustment"
|
||||
);
|
||||
expect(fetchCall).toBeDefined();
|
||||
const body = JSON.parse(fetchCall![1].body as string);
|
||||
expect(body.stockAdjustment).toBe(-1); // NOT -2 (the old bug)
|
||||
});
|
||||
|
||||
it("stock correction uses correct base for blister type medications", async () => {
|
||||
// Ensure blister type still works correctly after the bottle fix
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true });
|
||||
|
||||
const blisterMed: Medication = {
|
||||
id: 2,
|
||||
name: "Blister Med",
|
||||
packageType: "blister",
|
||||
packCount: 1,
|
||||
blistersPerPack: 5,
|
||||
pillsPerBlister: 5,
|
||||
looseTablets: 0,
|
||||
stockAdjustment: 1,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2026-01-30T21:07:00" }],
|
||||
updatedAt: null,
|
||||
};
|
||||
|
||||
// getMedTotal for blister = 1*5*5 + 0 + 1 = 26
|
||||
// getPackageSize for blister = 1*5*5 + 0 = 25
|
||||
|
||||
const mockLoadMeds = vi.fn();
|
||||
const { result } = renderHook(() => useRefill());
|
||||
|
||||
// User sees 26 pills → 5 full blisters (5pills each) + 1 partial
|
||||
act(() => {
|
||||
result.current.openEditStockModal(blisterMed, {
|
||||
all: [{ name: "Blister Med", medsLeft: 26, daysLeft: 26 }] as Coverage[],
|
||||
});
|
||||
});
|
||||
|
||||
// User changes to 27 (+1): 5 full + 2 partial
|
||||
act(() => {
|
||||
result.current.setEditStockFullBlisters(5);
|
||||
result.current.setEditStockPartialBlisterPills(2);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitStockCorrection(2, blisterMed, mockLoadMeds);
|
||||
});
|
||||
|
||||
// desiredTotal = 5 * 5 + 2 = 27
|
||||
// baseTotal = getPackageSize(blister) = 1*5*5 + 0 = 25
|
||||
// newStockAdjustment = 27 - 25 = 2
|
||||
// → getMedTotal = 25 + 2 = 27 ✓
|
||||
const fetchCall = (global.fetch as ReturnType<typeof vi.fn>).mock.calls.find(
|
||||
(call: [string, RequestInit]) => call[0] === "/api/medications/2/stock-adjustment"
|
||||
);
|
||||
expect(fetchCall).toBeDefined();
|
||||
const body = JSON.parse(fetchCall![1].body as string);
|
||||
expect(body.stockAdjustment).toBe(2);
|
||||
});
|
||||
|
||||
it("allows setting state directly", () => {
|
||||
const { result } = renderHook(() => useRefill());
|
||||
|
||||
|
||||
@@ -483,6 +483,187 @@ describe("calculateCoverage", () => {
|
||||
// medsLeft = 23 - 1 = 22
|
||||
expect(result.all[0].medsLeft).toBe(22);
|
||||
});
|
||||
|
||||
it("manual mode: stock correction excludes same-day taken doses", () => {
|
||||
// BUG FIX: In manual mode, doses taken on the same day as a stock correction
|
||||
// were counted as consumed (>= comparison with date-only timestamps).
|
||||
// The user already accounted for today's consumption when setting the stock count.
|
||||
//
|
||||
// Scenario: User has 110 pills, took 1 dose today, corrects to 111.
|
||||
// Bug: medsLeft = 111 - 1 = 110 (today's dose counted)
|
||||
// Fix: medsLeft = 111 - 0 = 111 (today's dose excluded)
|
||||
const correctionTime = new Date("2024-03-15T12:00:00Z");
|
||||
const todayMidnight = new Date("2024-03-15T00:00:00").getTime();
|
||||
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "DailyMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 14,
|
||||
pillsPerBlister: 14,
|
||||
looseTablets: 0,
|
||||
stockAdjustment: -85, // 196 - 85 = 111 pills
|
||||
lastStockCorrectionAt: correctionTime.toISOString(),
|
||||
takenBy: [],
|
||||
blisters: [
|
||||
{
|
||||
usage: 1,
|
||||
every: 1,
|
||||
start: "2024-01-01T08:00:00",
|
||||
},
|
||||
],
|
||||
updatedAt: correctionTime.toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
// User took a dose today (before the correction)
|
||||
const takenDoses = new Set([`1-0-${todayMidnight}`]);
|
||||
|
||||
const result = calculateCoverage(meds, [], "en", 7, "manual", takenDoses);
|
||||
|
||||
expect(result.all).toHaveLength(1);
|
||||
// getMedTotal = 196 - 85 = 111
|
||||
// Today's taken dose should NOT be counted (same day as correction)
|
||||
expect(result.all[0].medsLeft).toBe(111);
|
||||
});
|
||||
|
||||
it("manual mode: stock correction counts next-day taken doses", () => {
|
||||
// After a stock correction, doses taken the next day SHOULD be counted.
|
||||
const correctionTime = new Date("2024-03-14T12:00:00Z");
|
||||
const march15Midnight = new Date("2024-03-15T00:00:00").getTime();
|
||||
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "DailyMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
stockAdjustment: -7, // 30 - 7 = 23 pills
|
||||
lastStockCorrectionAt: correctionTime.toISOString(),
|
||||
takenBy: [],
|
||||
blisters: [
|
||||
{
|
||||
usage: 1,
|
||||
every: 1,
|
||||
start: "2024-03-01T08:00:00",
|
||||
},
|
||||
],
|
||||
updatedAt: correctionTime.toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
// User takes dose on March 15 (day after correction on March 14)
|
||||
const takenDoses = new Set([`1-0-${march15Midnight}`]);
|
||||
|
||||
const result = calculateCoverage(meds, [], "en", 7, "manual", takenDoses);
|
||||
|
||||
expect(result.all).toHaveLength(1);
|
||||
// getMedTotal = 30 - 7 = 23
|
||||
// March 15 dose should be counted (day after correction)
|
||||
expect(result.all[0].medsLeft).toBe(22);
|
||||
});
|
||||
|
||||
it("manual mode: no stock correction counts all taken doses", () => {
|
||||
// Without any stock correction, all taken doses should be counted
|
||||
const march14Midnight = new Date("2024-03-14T00:00:00").getTime();
|
||||
const march15Midnight = new Date("2024-03-15T00:00:00").getTime();
|
||||
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "DailyMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: [],
|
||||
blisters: [
|
||||
{
|
||||
usage: 1,
|
||||
every: 1,
|
||||
start: "2024-03-01T08:00:00",
|
||||
},
|
||||
],
|
||||
updatedAt: null,
|
||||
},
|
||||
];
|
||||
|
||||
// User took doses on March 14 and 15
|
||||
const takenDoses = new Set([`1-0-${march14Midnight}`, `1-0-${march15Midnight}`]);
|
||||
|
||||
const result = calculateCoverage(meds, [], "en", 7, "manual", takenDoses);
|
||||
|
||||
expect(result.all).toHaveLength(1);
|
||||
// Both doses should be counted: medsLeft = 30 - 2 = 28
|
||||
expect(result.all[0].medsLeft).toBe(28);
|
||||
});
|
||||
|
||||
it("manual mode: stock correction with multiple medications", () => {
|
||||
// Regression test: 3 medications (daily, daily, weekly).
|
||||
// Stock correction on all 3. The daily ones have same-day taken doses.
|
||||
const correctionTime = new Date("2024-03-15T12:00:00Z");
|
||||
const todayMidnight = new Date("2024-03-15T00:00:00").getTime();
|
||||
|
||||
const meds: Medication[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "DailyMed1",
|
||||
packCount: 1,
|
||||
blistersPerPack: 14,
|
||||
pillsPerBlister: 14,
|
||||
looseTablets: 0,
|
||||
stockAdjustment: -85, // 196 - 85 = 111
|
||||
lastStockCorrectionAt: correctionTime.toISOString(),
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-01-01T08:00:00" }],
|
||||
updatedAt: correctionTime.toISOString(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "DailyMed2",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
stockAdjustment: -10, // 30 - 10 = 20
|
||||
lastStockCorrectionAt: correctionTime.toISOString(),
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: "2024-01-01T09:00:00" }],
|
||||
updatedAt: correctionTime.toISOString(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "WeeklyMed",
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 10,
|
||||
looseTablets: 0,
|
||||
stockAdjustment: -2, // 10 - 2 = 8
|
||||
lastStockCorrectionAt: correctionTime.toISOString(),
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 7, start: "2024-01-05T10:00:00" }],
|
||||
updatedAt: correctionTime.toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
// Daily meds have same-day taken doses, weekly med does not
|
||||
const takenDoses = new Set([`1-0-${todayMidnight}`, `2-0-${todayMidnight}`]);
|
||||
|
||||
const result = calculateCoverage(meds, [], "en", 7, "manual", takenDoses);
|
||||
|
||||
expect(result.all).toHaveLength(3);
|
||||
const daily1 = result.all.find((c) => c.name === "DailyMed1")!;
|
||||
const daily2 = result.all.find((c) => c.name === "DailyMed2")!;
|
||||
const weekly = result.all.find((c) => c.name === "WeeklyMed")!;
|
||||
|
||||
// All three should reflect full stock (same-day doses excluded)
|
||||
expect(daily1.medsLeft).toBe(111);
|
||||
expect(daily2.medsLeft).toBe(20);
|
||||
expect(weekly.medsLeft).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStockStatus", () => {
|
||||
|
||||
@@ -202,7 +202,7 @@ export function calculateCoverage(
|
||||
if (
|
||||
!Number.isNaN(blisterStartDateOnly) &&
|
||||
doseTimestamp >= blisterStartDateOnly &&
|
||||
doseTimestamp >= stockCorrectionDateOnly
|
||||
doseTimestamp > stockCorrectionDateOnly
|
||||
) {
|
||||
consumed += blisters[blisterIdx].usage;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user