fix(refill): stabilize stock and amount package semantics
This commit is contained in:
@@ -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" });
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user