fix(refill): stabilize stock and amount package semantics

This commit is contained in:
Daniel Volz
2026-05-08 11:03:25 +02:00
parent b838f0e8ea
commit 277fc3e686
23 changed files with 1696 additions and 335 deletions
+401 -7
View File
@@ -308,10 +308,10 @@ describe("E2E Tests with Real Routes", () => {
expect(response.json().error).toBe("Access denied to medication");
});
it("should aggregate taken/dismissed doses and refill history", async () => {
it("should aggregate taken/skipped doses and refill history", async () => {
const medId = await createMedication(testClient, userId, "Report Med", ["Daniel"]);
// One taken dose and one dismissed dose for the same medication
// One taken dose and one skipped dose for the same medication
await testClient.execute({
sql: `INSERT INTO dose_tracking (user_id, dose_id, taken_at, dismissed)
VALUES (?, ?, ?, 0)`,
@@ -338,13 +338,14 @@ describe("E2E Tests with Real Routes", () => {
expect(response.statusCode).toBe(200);
const data = response.json();
expect(data[medId].dosesTaken).toBe(1);
expect(data[medId].dosesDismissed).toBe(1);
expect(data[medId].dosesSkipped).toBe(1);
expect(data[medId].firstDoseAt).toBe(new Date(1735344000 * 1000).toISOString());
expect(data[medId].lastDoseAt).toBe(new Date(1735344000 * 1000).toISOString());
expect(data[medId].refills).toHaveLength(1);
expect(data[medId].refills[0]).toMatchObject({
packsAdded: 2,
loosePillsAdded: 5,
quantityAdded: 7,
usedPrescription: true,
});
});
@@ -376,6 +377,7 @@ describe("E2E Tests with Real Routes", () => {
expect(data[medId].refills[0]).toMatchObject({
packsAdded: 1,
loosePillsAdded: 0,
quantityAdded: 1,
usedPrescription: false,
});
});
@@ -2443,6 +2445,81 @@ describe("E2E Tests with Real Routes", () => {
expect(med.stockAdjustment).toBe(0);
});
it("should align liquid amount-base fields for stale stock-adjustment clients before refill", async () => {
await testClient.execute({
sql: `INSERT INTO user_settings (user_id, stock_calculation_mode) VALUES (?, 'manual')`,
args: [userId],
});
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload: {
name: "Liquid Stale Client Stock Correction",
medicationForm: "liquid",
packageType: "liquid_container",
doseUnit: "ml",
packCount: 7,
packageAmountValue: 150,
packageAmountUnit: "ml",
blistersPerPack: 1,
pillsPerBlister: 1,
totalPills: 1050,
looseTablets: 1050,
blisters: [{ usage: 5, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
});
expect(createResponse.statusCode).toBe(200);
const medId = createResponse.json().id;
const correctionResponse = await app.inject({
method: "PATCH",
url: `/medications/${medId}/stock-adjustment`,
payload: {
stockAdjustment: 0,
packCount: 1,
totalPills: 150,
},
});
expect(correctionResponse.statusCode).toBe(200);
const afterCorrectionResponse = await app.inject({ method: "GET", url: "/medications" });
expect(afterCorrectionResponse.statusCode).toBe(200);
const correctedMed = afterCorrectionResponse.json().find((item: Record<string, unknown>) => item.id === medId);
expect(correctedMed).toBeTruthy();
expect(correctedMed.packCount).toBe(1);
expect(correctedMed.totalPills).toBe(150);
expect(correctedMed.looseTablets).toBe(150);
expect(correctedMed.stockAdjustment).toBe(0);
const refillResponse = await app.inject({
method: "POST",
url: `/medications/${medId}/refill`,
payload: { packsAdded: 1, loosePillsAdded: 0 },
});
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
expect(refillData.refill.quantityAdded).toBe(150);
expect(refillData.newStock.packCount).toBe(2);
expect(refillData.newStock.looseTablets).toBe(300);
expect(refillData.newStock.totalPills).toBe(300);
const historyResponse = await app.inject({
method: "GET",
url: `/medications/${medId}/refills`,
});
expect(historyResponse.statusCode).toBe(200);
expect(historyResponse.json()[0].quantityAdded).toBe(150);
const afterRefillResponse = await app.inject({ method: "GET", url: "/medications" });
expect(afterRefillResponse.statusCode).toBe(200);
const refilledMed = afterRefillResponse.json().find((item: Record<string, unknown>) => item.id === medId);
expect(refilledMed).toBeTruthy();
expect(refilledMed.packCount).toBe(2);
expect(refilledMed.totalPills).toBe(300);
expect(refilledMed.looseTablets).toBe(300);
});
it("should persist stockAdjustment in GET /medications", async () => {
const createResponse = await app.inject({
method: "POST",
@@ -3048,6 +3125,47 @@ describe("E2E Tests with Real Routes", () => {
blisters: [{ usage: 2, every: 1, start: "2025-01-01T08:00:00.000Z" }],
};
async function expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill,
expectedQuantityAdded,
expectedPacksAdded,
expectedAmountPerPackage,
}: {
medId: number;
refillData: {
refill: { packsAdded: number; quantityAdded: number; totalPillsAdded: number };
newStock: { packCount: number; totalPills: number; looseTablets: number };
};
visibleStockBeforeRefill: number;
expectedQuantityAdded: number;
expectedPacksAdded: number;
expectedAmountPerPackage?: number;
}) {
expect(refillData.refill.packsAdded).toBe(expectedPacksAdded);
expect(refillData.refill.quantityAdded).toBe(expectedQuantityAdded);
expect(refillData.refill.totalPillsAdded).toBe(expectedQuantityAdded);
expect(refillData.newStock.totalPills - visibleStockBeforeRefill).toBe(expectedQuantityAdded);
const historyResponse = await app.inject({
method: "GET",
url: `/medications/${medId}/refills`,
});
expect(historyResponse.statusCode).toBe(200);
expect(historyResponse.json()[0]).toMatchObject({
packsAdded: expectedPacksAdded,
quantityAdded: expectedQuantityAdded,
totalPillsAdded: expectedQuantityAdded,
});
if (expectedAmountPerPackage) {
expect(refillData.newStock.packCount).toBe(
Math.max(1, Math.ceil(refillData.newStock.totalPills / expectedAmountPerPackage))
);
}
}
it("should create and return bottle type medication", async () => {
const response = await app.inject({
method: "POST",
@@ -3241,6 +3359,196 @@ describe("E2E Tests with Real Routes", () => {
});
});
it.each([
{
name: "bottle",
payload: {
...bottleMedication,
totalPills: 100,
looseTablets: 10,
},
refillPayload: { packsAdded: 0, loosePillsAdded: 100 },
expectedVisibleStockBeforeRefill: 4,
expectedQuantityAdded: 100,
expectedResponsePacksAdded: 0,
expectedPackCount: 0,
expectedLooseTablets: 104,
expectedTotalPills: 104,
expectedPersistedTotalPills: 100,
expectedStockAdjustment: 0,
},
{
name: "blister",
payload: {
...blisterMedication,
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 10,
looseTablets: 0,
},
refillPayload: { packsAdded: 1, loosePillsAdded: 0 },
expectedVisibleStockBeforeRefill: 4,
expectedQuantityAdded: 10,
expectedResponsePacksAdded: 1,
expectedPackCount: 2,
expectedLooseTablets: 0,
expectedTotalPills: 14,
expectedPersistedTotalPills: null,
expectedStockAdjustment: -6,
},
{
name: "liquid_container",
payload: {
...liquidContainerMedication,
packCount: 1,
packageAmountValue: 100,
packageAmountUnit: "ml",
totalPills: 10,
looseTablets: 10,
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
refillPayload: { packsAdded: 1, loosePillsAdded: 0 },
expectedVisibleStockBeforeRefill: 4,
expectedQuantityAdded: 100,
expectedResponsePacksAdded: 1,
expectedAmountPerPackage: 100,
expectedPackCount: 2,
expectedLooseTablets: 104,
expectedTotalPills: 104,
expectedPersistedTotalPills: 104,
expectedStockAdjustment: 0,
},
])("should refill from current visible stock after prior consumption for $name", async ({
payload,
refillPayload,
expectedVisibleStockBeforeRefill,
expectedQuantityAdded,
expectedResponsePacksAdded,
expectedAmountPerPackage,
expectedPackCount,
expectedLooseTablets,
expectedTotalPills,
expectedPersistedTotalPills,
expectedStockAdjustment,
}) => {
await testClient.execute({
sql: `INSERT OR REPLACE INTO user_settings (user_id, stock_calculation_mode) VALUES (?, 'manual')`,
args: [userId],
});
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload,
});
expect(createResponse.statusCode).toBe(200);
const medId = createResponse.json().id;
for (let day = 1; day <= 6; day += 1) {
const doseDateOnlyMs = new Date(`2025-01-0${day}T00:00:00.000Z`).getTime();
const takenAtMs = new Date(`2025-01-0${day}T10:00:00.000Z`).getTime();
await testClient.execute({
sql: `INSERT INTO dose_tracking (user_id, dose_id, taken_at, dismissed)
VALUES (?, ?, ?, 0)`,
args: [userId, `${medId}-0-${doseDateOnlyMs}`, takenAtMs],
});
}
const refillResponse = await app.inject({
method: "POST",
url: `/medications/${medId}/refill`,
payload: refillPayload,
});
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
await expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill: expectedVisibleStockBeforeRefill,
expectedQuantityAdded,
expectedPacksAdded: expectedResponsePacksAdded,
expectedAmountPerPackage,
});
expect(refillData.newStock.packCount).toBe(expectedPackCount);
expect(refillData.newStock.looseTablets).toBe(expectedLooseTablets);
expect(refillData.newStock.totalPills).toBe(expectedTotalPills);
const medsResponse = await app.inject({ method: "GET", url: "/medications" });
expect(medsResponse.statusCode).toBe(200);
const med = medsResponse.json().find((item: Record<string, unknown>) => item.id === medId);
expect(med).toBeTruthy();
expect(med.packCount).toBe(expectedPackCount);
expect(med.looseTablets).toBe(expectedLooseTablets);
expect(med.totalPills).toBe(expectedPersistedTotalPills);
expect(med.stockAdjustment).toBe(expectedStockAdjustment);
});
it("should refill tube stock from the corrected visible baseline", async () => {
await testClient.execute({
sql: `INSERT OR REPLACE INTO user_settings (user_id, stock_calculation_mode) VALUES (?, 'manual')`,
args: [userId],
});
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload: {
...tubeMedication,
packCount: 1,
packageAmountValue: 80,
packageAmountUnit: "g",
totalPills: 10,
looseTablets: 10,
blisters: [{ usage: 1, every: 1, start: "2025-01-01T08:00:00.000Z" }],
},
});
expect(createResponse.statusCode).toBe(200);
const medId = createResponse.json().id;
const correctionResponse = await app.inject({
method: "PATCH",
url: `/medications/${medId}/stock-adjustment`,
payload: {
stockAdjustment: -6,
looseTablets: 10,
totalPills: 10,
packageAmountValue: 80,
packCount: 1,
},
});
expect(correctionResponse.statusCode).toBe(200);
const refillResponse = await app.inject({
method: "POST",
url: `/medications/${medId}/refill`,
payload: { packsAdded: 1, loosePillsAdded: 0 },
});
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
await expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill: 4,
expectedQuantityAdded: 80,
expectedPacksAdded: 1,
expectedAmountPerPackage: 80,
});
expect(refillData.newStock.packCount).toBe(2);
expect(refillData.newStock.looseTablets).toBe(84);
expect(refillData.newStock.totalPills).toBe(84);
const medsResponse = await app.inject({ method: "GET", url: "/medications" });
expect(medsResponse.statusCode).toBe(200);
const med = medsResponse.json().find((item: Record<string, unknown>) => item.id === medId);
expect(med).toBeTruthy();
expect(med.packCount).toBe(2);
expect(med.looseTablets).toBe(84);
expect(med.totalPills).toBe(84);
expect(med.stockAdjustment).toBe(0);
});
it("should calculate correct refill totalPillsAdded for blister type", async () => {
const createResponse = await app.inject({
method: "POST",
@@ -3272,6 +3580,11 @@ describe("E2E Tests with Real Routes", () => {
});
it("should keep liquid_container refill additive and preserve amount baseline", async () => {
await testClient.execute({
sql: `INSERT INTO user_settings (user_id, stock_calculation_mode) VALUES (?, 'manual')`,
args: [userId],
});
const createResponse = await app.inject({
method: "POST",
url: "/medications",
@@ -3294,9 +3607,15 @@ describe("E2E Tests with Real Routes", () => {
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
expect(refillData.refill.packsAdded).toBe(1);
await expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill: 180,
expectedQuantityAdded: 180,
expectedPacksAdded: 1,
expectedAmountPerPackage: 180,
});
expect(refillData.refill.loosePillsAdded).toBe(180);
expect(refillData.refill.totalPillsAdded).toBe(180);
expect(refillData.newStock.totalPills).toBe(360);
const medsResponse = await app.inject({ method: "GET", url: "/medications" });
@@ -3307,6 +3626,54 @@ describe("E2E Tests with Real Routes", () => {
expect(med.looseTablets).toBe(360);
});
it("should normalize liquid_container packCount to the full visible stock after refill", async () => {
await testClient.execute({
sql: `INSERT INTO user_settings (user_id, stock_calculation_mode) VALUES (?, 'manual')`,
args: [userId],
});
const createResponse = await app.inject({
method: "POST",
url: "/medications",
payload: {
...liquidContainerMedication,
packCount: 0,
packageAmountValue: 150,
totalPills: 300,
looseTablets: 300,
},
});
expect(createResponse.statusCode).toBe(200);
const medId = createResponse.json().id;
const refillResponse = await app.inject({
method: "POST",
url: `/medications/${medId}/refill`,
payload: { packsAdded: 5, loosePillsAdded: 0 },
});
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
await expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill: 300,
expectedQuantityAdded: 750,
expectedPacksAdded: 5,
expectedAmountPerPackage: 150,
});
expect(refillData.newStock.packCount).toBe(7);
expect(refillData.newStock.totalPills).toBe(1050);
const medsResponse = await app.inject({ method: "GET", url: "/medications" });
expect(medsResponse.statusCode).toBe(200);
const med = medsResponse.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med).toBeTruthy();
expect(med.packCount).toBe(7);
expect(med.totalPills).toBe(1050);
expect(med.looseTablets).toBe(1050);
});
it.each([
{
name: "liquid_container",
@@ -3323,10 +3690,12 @@ describe("E2E Tests with Real Routes", () => {
prescriptionLowRefillThreshold: 1,
},
refillPayload: { packsAdded: 0, loosePillsAdded: 180, usePrescription: true },
expectedVisibleStockBeforeRefill: 180,
expectedPacksAdded: 1,
expectedLooseAdded: 180,
expectedRemainingRefills: 1,
expectedTotalPills: 360,
expectedAmountPerPackage: 180,
},
{
name: "tube",
@@ -3338,19 +3707,28 @@ describe("E2E Tests with Real Routes", () => {
prescriptionLowRefillThreshold: 1,
},
refillPayload: { packsAdded: 0, loosePillsAdded: 80, usePrescription: true },
expectedVisibleStockBeforeRefill: 80,
expectedPacksAdded: 2,
expectedLooseAdded: 80,
expectedRemainingRefills: 1,
expectedTotalPills: 160,
expectedAmountPerPackage: 40,
},
])("should derive amount-based refill counts and decrement prescription remaining refills for $name", async ({
payload,
refillPayload,
expectedVisibleStockBeforeRefill,
expectedPacksAdded,
expectedLooseAdded,
expectedRemainingRefills,
expectedTotalPills,
expectedAmountPerPackage,
}) => {
await testClient.execute({
sql: `INSERT OR REPLACE INTO user_settings (user_id, stock_calculation_mode) VALUES (?, 'manual')`,
args: [userId],
});
const createResponse = await app.inject({
method: "POST",
url: "/medications",
@@ -3367,8 +3745,17 @@ describe("E2E Tests with Real Routes", () => {
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
await expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill: expectedVisibleStockBeforeRefill,
expectedQuantityAdded: expectedLooseAdded,
expectedPacksAdded,
expectedAmountPerPackage,
});
expect(refillData.refill.packsAdded).toBe(expectedPacksAdded);
expect(refillData.refill.loosePillsAdded).toBe(expectedLooseAdded);
expect(refillData.refill.quantityAdded).toBe(expectedLooseAdded);
expect(refillData.refill.totalPillsAdded).toBe(expectedLooseAdded);
expect(refillData.prescription.used).toBe(true);
expect(refillData.prescription.remainingRefills).toBe(expectedRemainingRefills);
@@ -3382,6 +3769,7 @@ describe("E2E Tests with Real Routes", () => {
expect(historyResponse.json()[0]).toMatchObject({
packsAdded: expectedPacksAdded,
loosePillsAdded: expectedLooseAdded,
quantityAdded: expectedLooseAdded,
usedPrescription: true,
});
});
@@ -3403,9 +3791,15 @@ describe("E2E Tests with Real Routes", () => {
expect(refillResponse.statusCode).toBe(200);
const refillData = refillResponse.json();
expect(refillData.refill.packsAdded).toBe(1);
await expectRefillInvariants({
medId,
refillData,
visibleStockBeforeRefill: 80,
expectedQuantityAdded: 40,
expectedPacksAdded: 1,
expectedAmountPerPackage: 40,
});
expect(refillData.refill.loosePillsAdded).toBe(40);
expect(refillData.refill.totalPillsAdded).toBe(40);
expect(refillData.newStock.totalPills).toBe(120);
const medsResponse = await app.inject({ method: "GET", url: "/medications" });
+40
View File
@@ -411,6 +411,7 @@ describe("Export/Import API", () => {
expect(data.settings.notificationEmail).toBe("test@example.com");
expect(data.settings.language).toBe("de");
expect(data.settings.lowStockDays).toBe(14);
expect(data.settings.shareStockStatus).toBeUndefined();
});
it("should exclude sensitive data by default", async () => {
@@ -557,6 +558,45 @@ describe("Export/Import API", () => {
expect(result.rows[0].loose_tablets).toBe(5);
});
it("accepts legacy shareStockStatus in imported settings but does not export or use it", async () => {
const importData = {
version: "1.0",
exportedAt: new Date().toISOString(),
medications: [],
doseHistory: [],
refillHistory: [],
settings: {
language: "de",
stockCalculationMode: "automatic",
shareStockStatus: false,
},
shareLinks: [],
};
const importResponse = await ctx.app.inject({
method: "POST",
url: "/import",
payload: importData,
});
expect(importResponse.statusCode).toBe(200);
const exportResponse = await ctx.app.inject({
method: "GET",
url: "/export",
});
expect(exportResponse.statusCode).toBe(200);
expect(exportResponse.json().settings.shareStockStatus).toBeUndefined();
const settingsRow = await ctx.client.execute({
sql: "SELECT share_medication_overview, share_stock_status FROM user_settings WHERE user_id = ?",
args: [userId],
});
expect(settingsRow.rows[0].share_medication_overview).toBe(0);
expect(settingsRow.rows[0].share_stock_status).toBe(1);
});
it("should replace existing data on import", async () => {
// Create existing medication
await createTestMedication(ctx.client, {
+96 -4
View File
@@ -16,6 +16,8 @@ const { testClient, testDb, mockedEnv, nodemailerSendMail, fetchMock } = vi.hois
OIDC_ENABLED: false,
OIDC_PROVIDER_NAME: "SSO",
NODE_ENV: "test",
PUBLIC_APP_URL: "https://app.example.com",
CORS_ORIGINS: "https://app.example.com",
};
return {
testClient: client,
@@ -351,7 +353,7 @@ describe("Real route coverage: settings/export/report", () => {
});
it("POST /settings/test-shoutrrr returns 200 for a valid ntfy target", async () => {
fetchMock.mockResolvedValue({ ok: true });
fetchMock.mockResolvedValue({ ok: true, json: () => Promise.resolve({ id: "ntfy-test-message-id" }) });
const response = await app.inject({
method: "POST",
@@ -361,6 +363,44 @@ describe("Real route coverage: settings/export/report", () => {
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ success: true, message: "Test notification sent successfully" });
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, requestInit] = fetchMock.mock.calls[0] ?? [];
const headers = (requestInit?.headers ?? {}) as Record<string, string>;
expect(headers["X-Sequence-ID"]).toEqual(expect.stringMatching(/^medassist-/));
expect(JSON.parse(headers.Actions ?? "[]")).toEqual([
{
action: "http",
label: "Take",
url: expect.stringMatching(/^https:\/\/app\.example\.com\/api\/notification-actions\//),
method: "POST",
clear: false,
},
{
action: "http",
label: "Skip",
url: expect.stringMatching(/^https:\/\/app\.example\.com\/api\/notification-actions\//),
method: "POST",
clear: false,
},
{
action: "view",
label: "View",
url: "https://app.example.com/dashboard",
clear: false,
},
]);
const groups = await testClient.execute("SELECT COUNT(*) AS count FROM notification_action_groups");
expect(Number(groups.rows[0].count)).toBe(1);
const storedGroup = await testClient.execute(
"SELECT ntfy_original_message_id FROM notification_action_groups LIMIT 1"
);
expect(storedGroup.rows).toEqual([expect.objectContaining({ ntfy_original_message_id: "ntfy-test-message-id" })]);
const tokens = await testClient.execute("SELECT COUNT(*) AS count FROM notification_action_tokens");
expect(Number(tokens.rows[0].count)).toBe(3);
});
it("sendShoutrrrNotification blocks localhost/private targets", async () => {
@@ -370,11 +410,12 @@ describe("Real route coverage: settings/export/report", () => {
});
it("sendShoutrrrNotification handles ntfy auth and safe URL reconstruction", async () => {
fetchMock.mockResolvedValue({ ok: true });
fetchMock.mockResolvedValue({ ok: true, json: () => Promise.resolve({ id: "ntfy-message-id" }) });
const result = await sendShoutrrrNotification("ntfy://user:pass@ntfy.sh/mytopic", "Title ä", "Message");
expect(result.success).toBe(true);
expect(result.providerMessageId).toBe("ntfy-message-id");
expect(fetchMock).toHaveBeenCalledWith(
"https://ntfy.sh/mytopic",
expect.objectContaining({
@@ -589,8 +630,35 @@ describe("Real route coverage: settings/export/report", () => {
expect(response.statusCode).toBe(200);
const body = response.json();
expect(body[medId].dosesTaken).toBe(1);
expect(body[medId].dosesDismissed).toBe(1);
expect(body[medId].dosesSkipped).toBe(1);
expect(body[medId].refills).toHaveLength(1);
expect(body[medId].refills[0].quantityAdded).toBe(22);
});
it("POST /medications/report-data filters dose counts by takenBy suffix when requested", async () => {
const medId = await seedMedication("Report Filter Med");
await testClient.execute({
sql: "INSERT INTO dose_tracking (user_id, dose_id, taken_at, dismissed) VALUES (?, ?, ?, ?)",
args: [1, `${medId}-0-1700000000000-Alice`, 1700000000, 0],
});
await testClient.execute({
sql: "INSERT INTO dose_tracking (user_id, dose_id, taken_at, dismissed) VALUES (?, ?, ?, ?)",
args: [1, `${medId}-0-1700000600000-Alice`, 1700000600, 1],
});
await testClient.execute({
sql: "INSERT INTO dose_tracking (user_id, dose_id, taken_at, dismissed) VALUES (?, ?, ?, ?)",
args: [1, `${medId}-0-1700001200000-Bob`, 1700001200, 0],
});
const response = await app.inject({
method: "POST",
url: "/medications/report-data",
payload: { medicationIds: [medId], takenByFilter: ["Alice"] },
});
expect(response.statusCode).toBe(200);
const body = response.json();
expect(body[medId].dosesTaken).toBe(1);
expect(body[medId].dosesSkipped).toBe(1);
});
it("GET /export includes medications, settings, doseHistory and refillHistory", async () => {
@@ -621,7 +689,9 @@ describe("Real route coverage: settings/export/report", () => {
expect(body.medications).toHaveLength(1);
expect(body.doseHistory).toHaveLength(1);
expect(body.refillHistory).toHaveLength(1);
expect(body.refillHistory[0].quantityAdded).toBe(23);
expect(body.settings.language).toBe("de");
expect(body.settings.shareStockStatus).toBeUndefined();
expect(body.shareLinks).toHaveLength(1);
});
@@ -672,7 +742,15 @@ describe("Real route coverage: settings/export/report", () => {
},
],
doseHistory: [],
refillHistory: [],
refillHistory: [
{
medicationRef: "med-1",
packsAdded: 0,
quantityAdded: 4,
usedPrescription: false,
refillDate: "2026-01-02T08:00:00.000Z",
},
],
settings: {
emailEnabled: false,
notificationEmail: null,
@@ -708,10 +786,24 @@ describe("Real route coverage: settings/export/report", () => {
});
expect(valid.statusCode).toBe(200);
expect(valid.json().imported.medications).toBe(1);
expect(valid.json().imported.refillHistory).toBe(1);
const rows = await testClient.execute({
sql: "SELECT name FROM medications WHERE user_id = 1",
});
expect(rows.rows[0].name).toBe("Imported Med");
const refillRows = await testClient.execute({
sql: "SELECT packs_added, loose_pills_added FROM refill_history WHERE user_id = 1",
});
expect(refillRows.rows).toHaveLength(1);
expect(refillRows.rows[0].packs_added).toBe(0);
expect(refillRows.rows[0].loose_pills_added).toBe(4);
const importedSettings = await testClient.execute({
sql: "SELECT share_medication_overview, share_stock_status FROM user_settings WHERE user_id = 1",
});
expect(importedSettings.rows[0].share_medication_overview).toBe(0);
expect(importedSettings.rows[0].share_stock_status).toBe(1);
});
});