mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
feat(medications): carry manufacturer and printed strength on an inventory container
The native pen list renders these two as its headline and subhead. The server held neither, so a container registered on the web could not be shown there without fabricating an empty card, and the client kept its own copy locally with no back-fill possible. Both columns are additive and nullable (migration 0255). Every existing row is truthfully NULL — the values were never collected, and a plain supply row such as a blister pack or a bottle has no use for either. Strength stays free text rather than a parsed number + unit: pens state it per dose, per ml, or per cartridge depending on the product, and forcing a numeric split would lose which of the three was printed. On PATCH an omitted field leaves the stored value untouched, the same rule notes already followed, so a client that does not know the fields cannot blank what another populated; an explicit null still clears. The web add-container form collects both for a PEN, so the columns do not ship empty forever.
This commit is contained in:
1 parent
4ddc08f784
commit
4c3cc84daf
18 files changed
+391
-28
No files matched your search
@@ -183,3 +183,64 @@ describe("PATCH /api/medications/[id]/inventory/[itemId] — unitsRemaining stoc
|
||||
expect(res.status).toBe(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/medications/[id]/inventory/[itemId] — carton labelling", () => {
|
||||
it("updates manufacturer + doseStrength when supplied", async () => {
|
||||
vi.mocked(prisma.medicationInventoryItem.update).mockResolvedValue({
|
||||
id: "i1",
|
||||
state: "ACTIVE",
|
||||
} as never);
|
||||
|
||||
const res = await PATCH(
|
||||
patchReq({
|
||||
manufacturer: "Example Pharma",
|
||||
doseStrength: "5 mg/0.5 ml",
|
||||
}),
|
||||
ROUTE_CTX,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const arg = vi.mocked(prisma.medicationInventoryItem.update).mock
|
||||
.calls[0]?.[0] as unknown as {
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
expect(arg.data.manufacturer).toBe("Example Pharma");
|
||||
expect(arg.data.doseStrength).toBe("5 mg/0.5 ml");
|
||||
});
|
||||
|
||||
it("leaves both untouched when the request omits them", async () => {
|
||||
// Absent must mean "I have nothing to say", never "blank it" — a client
|
||||
// that does not know the fields must not wipe what another populated.
|
||||
vi.mocked(prisma.medicationInventoryItem.update).mockResolvedValue({
|
||||
id: "i1",
|
||||
state: "ACTIVE",
|
||||
} as never);
|
||||
|
||||
const res = await PATCH(patchReq({ unitsRemaining: 2 }), ROUTE_CTX);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const arg = vi.mocked(prisma.medicationInventoryItem.update).mock
|
||||
.calls[0]?.[0] as unknown as {
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
expect("manufacturer" in arg.data).toBe(false);
|
||||
expect("doseStrength" in arg.data).toBe(false);
|
||||
});
|
||||
|
||||
it("clears a value on an explicit null", async () => {
|
||||
vi.mocked(prisma.medicationInventoryItem.update).mockResolvedValue({
|
||||
id: "i1",
|
||||
state: "ACTIVE",
|
||||
} as never);
|
||||
|
||||
const res = await PATCH(patchReq({ manufacturer: null }), ROUTE_CTX);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const arg = vi.mocked(prisma.medicationInventoryItem.update).mock
|
||||
.calls[0]?.[0] as unknown as {
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
expect(arg.data.manufacturer).toBeNull();
|
||||
expect("doseStrength" in arg.data).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -79,6 +79,8 @@ export const PATCH = apiHandler(
|
||||
markAsUsedUp,
|
||||
printedExpiry,
|
||||
unitsRemaining,
|
||||
manufacturer,
|
||||
doseStrength,
|
||||
notes,
|
||||
} = parsed.data;
|
||||
|
||||
@@ -151,6 +153,16 @@ export const PATCH = apiHandler(
|
||||
? {}
|
||||
: { notesEncrypted: encryptNote(notes), notes: null };
|
||||
|
||||
// Carton labelling follows the same absent-means-untouched rule as
|
||||
// `notes`: an omitted field must not blank a value the other client
|
||||
// populated. Built field by field, never spread from the parsed body.
|
||||
const labellingData: {
|
||||
manufacturer?: string | null;
|
||||
doseStrength?: string | null;
|
||||
} = {};
|
||||
if (manufacturer !== undefined) labellingData.manufacturer = manufacturer;
|
||||
if (doseStrength !== undefined) labellingData.doseStrength = doseStrength;
|
||||
|
||||
const updated = await prisma.medicationInventoryItem.update({
|
||||
where: { id: itemId },
|
||||
data: {
|
||||
@@ -159,6 +171,7 @@ export const PATCH = apiHandler(
|
||||
unitsRemaining: nextUnitsRemaining,
|
||||
printedExpiry: nextPrintedExpiry,
|
||||
expiresAt: nextExpiresAt,
|
||||
...labellingData,
|
||||
...notesData,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -471,3 +471,62 @@ describe("POST /api/medications/[id]/inventory — 422 multi-issue (v1.4.43 W6)"
|
||||
expect(body.details.issues.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/medications/[id]/inventory — carton labelling", () => {
|
||||
it("persists manufacturer + doseStrength when the client sends them", async () => {
|
||||
// The native pen list uses these two as its headline and subhead. With
|
||||
// nowhere on the server to hold them, a container registered on the web
|
||||
// could not be rendered there at all.
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.medication.findUnique).mockResolvedValue(MED_OK as never);
|
||||
vi.mocked(prisma.medicationInventoryItem.create).mockResolvedValue({
|
||||
id: "inv-new",
|
||||
state: "ACTIVE",
|
||||
} as never);
|
||||
|
||||
const res = await POST(
|
||||
jsonReq("http://localhost/api/medications/med-1/inventory", {
|
||||
unitsTotal: 4,
|
||||
containerType: "PEN",
|
||||
manufacturer: "Example Pharma",
|
||||
doseStrength: "5 mg/0.5 ml",
|
||||
}),
|
||||
{ params: Promise.resolve({ id: "med-1" }) },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(prisma.medicationInventoryItem.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
manufacturer: "Example Pharma",
|
||||
doseStrength: "5 mg/0.5 ml",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("stores null for both when the client omits them", async () => {
|
||||
// A plain supply row (blister, bottle) carries no carton labelling, and
|
||||
// every row that predates the columns is truthfully null.
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.medication.findUnique).mockResolvedValue(MED_OK as never);
|
||||
vi.mocked(prisma.medicationInventoryItem.create).mockResolvedValue({
|
||||
id: "inv-new",
|
||||
state: "ACTIVE",
|
||||
} as never);
|
||||
|
||||
const res = await POST(
|
||||
jsonReq("http://localhost/api/medications/med-1/inventory", {
|
||||
unitsTotal: 30,
|
||||
containerType: "BLISTER",
|
||||
}),
|
||||
{ params: Promise.resolve({ id: "med-1" }) },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(prisma.medicationInventoryItem.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
manufacturer: null,
|
||||
doseStrength: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -121,8 +121,15 @@ export const POST = apiHandler(
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
const { unitsTotal, containerType, printedExpiry, purchasedAt, notes } =
|
||||
parsed.data;
|
||||
const {
|
||||
unitsTotal,
|
||||
containerType,
|
||||
printedExpiry,
|
||||
purchasedAt,
|
||||
manufacturer,
|
||||
doseStrength,
|
||||
notes,
|
||||
} = parsed.data;
|
||||
|
||||
const created = await prisma.medicationInventoryItem.create({
|
||||
// The wire field `unitsTotal` counts UNITS (tablets / ampoules /
|
||||
@@ -135,6 +142,8 @@ export const POST = apiHandler(
|
||||
containerType: containerType ?? "OTHER",
|
||||
printedExpiry: printedExpiry ?? null,
|
||||
purchasedAt: purchasedAt ?? null,
|
||||
manufacturer: manufacturer ?? null,
|
||||
doseStrength: doseStrength ?? null,
|
||||
notes: notes ?? null,
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user