mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
A medication's units per dose, a schedule's override and a restored backup now accept any value above 0 and at most 100 with at most four decimal places, which is exactly what the Decimal(10,4) column stores. Before, only whole numbers 1-100 and the fractions 1/4, 1/3, 1/2, 2/3, 3/4 passed, so one and a half tablets could not be recorded. A restore now refuses a value no write route would accept. The medication editor keeps its buttons and adds an Other field that reads 1.5, 1,5, 1 1/2 and 1½, holds the step while the text does not read, and previews the value it will save. The supply tab's packaging dialog uses the same reader; it refused every non-whole value before, so a medication at half a tablet could not have its packaging saved. Cards and the supply tab show a whole number plus a fraction as 1½. The rule, the input reader and the display format live in one module without imports so the editor and the validators share them. Refs #1034
235 lines
6.7 KiB
TypeScript
235 lines
6.7 KiB
TypeScript
/**
|
||
* v1.16.10 — inventory + units-per-dose validation bounds.
|
||
*
|
||
* Pins the raised unit caps (100 → 1000 on container capacity and the
|
||
* stock correction; `dosesPerUnit` rides along), the new
|
||
* `unitsPerDose` (1–100) and `containerType` fields, and the symmetric
|
||
* request field names (`unitsTotal` / `unitsRemaining` — the response
|
||
* always carried them). The GLP-1 legacy ledger delta deliberately
|
||
* KEEPS its ±100 bound — it counts pens, not units.
|
||
*/
|
||
import { describe, expect, it } from "vitest";
|
||
|
||
import {
|
||
createInventoryItemSchema,
|
||
updateInventoryItemSchema,
|
||
createMedicationSchema,
|
||
updateMedicationSchema,
|
||
glp1InventoryPostSchema,
|
||
} from "../medication";
|
||
|
||
const MINIMAL_MEDICATION = {
|
||
name: "Med",
|
||
dose: "5 mg",
|
||
schedules: [
|
||
{
|
||
windowStart: "08:00",
|
||
windowEnd: "09:00",
|
||
},
|
||
],
|
||
};
|
||
|
||
describe("createInventoryItemSchema — unit cap 1000", () => {
|
||
it("accepts 1000 units", () => {
|
||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 1000 });
|
||
expect(r.success).toBe(true);
|
||
});
|
||
|
||
it("rejects 1001 units", () => {
|
||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 1001 });
|
||
expect(r.success).toBe(false);
|
||
});
|
||
|
||
it("rejects zero", () => {
|
||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 0 });
|
||
expect(r.success).toBe(false);
|
||
});
|
||
|
||
it("accepts every container type and defaults to absent", () => {
|
||
for (const ct of [
|
||
"PEN",
|
||
"AMPOULE",
|
||
"BLISTER",
|
||
"INHALER",
|
||
"BOTTLE",
|
||
"OTHER",
|
||
]) {
|
||
const r = createInventoryItemSchema.safeParse({
|
||
unitsTotal: 4,
|
||
containerType: ct,
|
||
});
|
||
expect(r.success).toBe(true);
|
||
}
|
||
const r = createInventoryItemSchema.safeParse({ unitsTotal: 4 });
|
||
expect(r.success).toBe(true);
|
||
expect(r.success && r.data.containerType).toBeUndefined();
|
||
});
|
||
|
||
it("rejects an unknown container type", () => {
|
||
const r = createInventoryItemSchema.safeParse({
|
||
unitsTotal: 4,
|
||
containerType: "BAG",
|
||
});
|
||
expect(r.success).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("updateInventoryItemSchema — stock correction cap 1000", () => {
|
||
it("accepts 1000", () => {
|
||
const r = updateInventoryItemSchema.safeParse({ unitsRemaining: 1000 });
|
||
expect(r.success).toBe(true);
|
||
});
|
||
|
||
it("rejects 1001", () => {
|
||
const r = updateInventoryItemSchema.safeParse({ unitsRemaining: 1001 });
|
||
expect(r.success).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("medication schemas — dosesPerUnit cap 1000 + unitsPerDose 1–100", () => {
|
||
it("create accepts dosesPerUnit 1000 and rejects 1001", () => {
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
dosesPerUnit: 1000,
|
||
}).success,
|
||
).toBe(true);
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
dosesPerUnit: 1001,
|
||
}).success,
|
||
).toBe(false);
|
||
});
|
||
|
||
it("update accepts dosesPerUnit 1000 and rejects 1001", () => {
|
||
expect(
|
||
updateMedicationSchema.safeParse({ dosesPerUnit: 1000 }).success,
|
||
).toBe(true);
|
||
expect(
|
||
updateMedicationSchema.safeParse({ dosesPerUnit: 1001 }).success,
|
||
).toBe(false);
|
||
});
|
||
|
||
it("create bounds unitsPerDose to 1–100", () => {
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: 1,
|
||
}).success,
|
||
).toBe(true);
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: 100,
|
||
}).success,
|
||
).toBe(true);
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: 0,
|
||
}).success,
|
||
).toBe(false);
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: 101,
|
||
}).success,
|
||
).toBe(false);
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: 100.5,
|
||
}).success,
|
||
).toBe(false);
|
||
});
|
||
|
||
it("update bounds unitsPerDose to 1–100", () => {
|
||
expect(updateMedicationSchema.safeParse({ unitsPerDose: 2 }).success).toBe(
|
||
true,
|
||
);
|
||
expect(updateMedicationSchema.safeParse({ unitsPerDose: 0 }).success).toBe(
|
||
false,
|
||
);
|
||
expect(
|
||
updateMedicationSchema.safeParse({ unitsPerDose: 101 }).success,
|
||
).toBe(false);
|
||
});
|
||
|
||
// v1.16.12 (#316) — fractional dosing: a curated set of split-pill
|
||
// fractions is accepted alongside the whole numbers.
|
||
it("accepts every curated unitsPerDose fraction on create + update", () => {
|
||
for (const v of [0.25, 0.3333, 0.5, 0.6667, 0.75]) {
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: v,
|
||
}).success,
|
||
).toBe(true);
|
||
expect(
|
||
updateMedicationSchema.safeParse({ unitsPerDose: v }).success,
|
||
).toBe(true);
|
||
}
|
||
});
|
||
|
||
// #1034 — a whole number plus a fraction (1½, 2¼), and any other value
|
||
// the Decimal(10,4) column stores exactly, is accepted on create + update.
|
||
it("accepts a mixed or free decimal unitsPerDose with up to four places", () => {
|
||
for (const v of [1.5, 2.25, 1.3333, 3.75, 0.1, 0.4, 1.2, 99.5]) {
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: v,
|
||
}).success,
|
||
).toBe(true);
|
||
expect(
|
||
updateMedicationSchema.safeParse({ unitsPerDose: v }).success,
|
||
).toBe(true);
|
||
}
|
||
});
|
||
|
||
it("rejects a unitsPerDose with more than four decimal places", () => {
|
||
for (const v of [1.23456, 0.33333, 0.00001]) {
|
||
expect(
|
||
createMedicationSchema.safeParse({
|
||
...MINIMAL_MEDICATION,
|
||
unitsPerDose: v,
|
||
}).success,
|
||
).toBe(false);
|
||
expect(
|
||
updateMedicationSchema.safeParse({ unitsPerDose: v }).success,
|
||
).toBe(false);
|
||
}
|
||
});
|
||
|
||
it("accepts a fractional remaining-stock correction (½-tablet leftover)", () => {
|
||
expect(
|
||
updateInventoryItemSchema.safeParse({ unitsRemaining: 29.5 }).success,
|
||
).toBe(true);
|
||
expect(
|
||
createInventoryItemSchema.safeParse({ unitsTotal: 1.5 }).success,
|
||
).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("glp1InventoryPostSchema — the legacy pen-ledger delta keeps ±100", () => {
|
||
it("accepts ±100 and rejects ±101", () => {
|
||
expect(
|
||
glp1InventoryPostSchema.safeParse({ delta: 100, reason: "purchased" })
|
||
.success,
|
||
).toBe(true);
|
||
expect(
|
||
glp1InventoryPostSchema.safeParse({ delta: -100, reason: "damaged" })
|
||
.success,
|
||
).toBe(true);
|
||
expect(
|
||
glp1InventoryPostSchema.safeParse({ delta: 101, reason: "purchased" })
|
||
.success,
|
||
).toBe(false);
|
||
expect(
|
||
glp1InventoryPostSchema.safeParse({ delta: -101, reason: "damaged" })
|
||
.success,
|
||
).toBe(false);
|
||
});
|
||
});
|