mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
fix(medications): clear the pending clock deadlines the repair would have missed
The first pass of migration 0276 restored the rows the in-use clock had already flipped, but every clock-less container still IN_USE carried the same clock-derived `expires_at`, most of them with the deadline still ahead. The daily expire scan would have worked through them over the following weeks and reproduced the whole defect one row at a time. A clock-less container's `expires_at` is now set from its printed expiry — usually nothing, which is how it stays out of that scan. Pens and ampoules keep theirs; their clock is real.
This commit is contained in:
1 parent
48b8071a67
commit
f226a907cb
2 files changed
+68
-14
No files matched your search
@@ -1,5 +1,6 @@
|
||||
-- Restore supply containers that the in-use clock wrote off although they
|
||||
-- never carried one.
|
||||
-- never carried one, and drop the deadlines that would have written off the
|
||||
-- rest of them over the following weeks.
|
||||
--
|
||||
-- Until this release the state machine started a 30-day post-opening clock
|
||||
-- for every container as soon as `first_use_at` was set, including the
|
||||
@@ -8,8 +9,26 @@
|
||||
-- the row went EXPIRED, its units dropped out of the available pool, and the
|
||||
-- supply card read "0 of 0 doses" beside a count of expired units.
|
||||
--
|
||||
-- The rows to restore are the ones that can ONLY have reached EXPIRED through
|
||||
-- that clock:
|
||||
-- Two things have to be undone. The rows already flipped need their state
|
||||
-- back, and every clock-less container still carries a persisted `expires_at`
|
||||
-- derived from that clock — including the ones whose deadline has not landed
|
||||
-- yet. Leaving those would let the daily expire scan repeat the whole defect
|
||||
-- on its own schedule.
|
||||
|
||||
-- (1) A clock-less container's only legitimate deadline is its printed
|
||||
-- expiry, so that is what `expires_at` becomes. For the common row that is
|
||||
-- NULL, which is precisely how it stays out of the daily expire scan. PEN and
|
||||
-- AMPOULE are left alone: their post-opening clock is real, so the deadline
|
||||
-- they carry is earned.
|
||||
UPDATE "medication_inventory_items"
|
||||
SET
|
||||
"expires_at" = "printed_expiry",
|
||||
"updated_at" = NOW()
|
||||
WHERE "container_type" NOT IN ('PEN', 'AMPOULE')
|
||||
AND "expires_at" IS DISTINCT FROM "printed_expiry";
|
||||
|
||||
-- (2) The rows that already flipped. They are the ones that can ONLY have
|
||||
-- reached EXPIRED through that clock:
|
||||
-- * `container_type` is one of the kinds that has no post-opening clock
|
||||
-- (everything except PEN and AMPOULE — see IN_USE_CLOCK_CONTAINER_TYPES
|
||||
-- in src/lib/medications/inventory/state-machine.ts),
|
||||
@@ -26,17 +45,15 @@
|
||||
-- * any row whose printed expiry has genuinely lapsed, whatever its kind;
|
||||
-- * rows with no units left (their state is a USED_UP question, not this
|
||||
-- one);
|
||||
-- * every other column. No date is invented and no printed expiry is
|
||||
-- fabricated: a row that recorded no expiry keeps recording none, which
|
||||
-- is why `expires_at` is set from `printed_expiry` rather than computed.
|
||||
-- * the state of every row that is not EXPIRED, and every other column.
|
||||
-- No date is invented and no printed expiry is fabricated: a row that
|
||||
-- recorded no expiry keeps recording none.
|
||||
UPDATE "medication_inventory_items"
|
||||
SET
|
||||
"state" = CASE
|
||||
WHEN "first_use_at" IS NOT NULL THEN 'IN_USE'::"medication_inventory_state"
|
||||
ELSE 'ACTIVE'::"medication_inventory_state"
|
||||
END,
|
||||
-- The only deadline a clock-less container can carry is its printed one.
|
||||
"expires_at" = "printed_expiry",
|
||||
"updated_at" = NOW()
|
||||
WHERE "state" = 'EXPIRED'
|
||||
AND "container_type" NOT IN ('PEN', 'AMPOULE')
|
||||
|
||||
@@ -224,25 +224,62 @@ describe("0276 inventory expiry container-type repair — integration", () => {
|
||||
});
|
||||
|
||||
it.each(["ACTIVE", "IN_USE", "USED_UP"] as const)(
|
||||
"leaves a %s row untouched",
|
||||
"leaves the state of a %s row alone",
|
||||
async (state) => {
|
||||
const prisma = getPrismaClient();
|
||||
const expiresAt = new Date(Date.now() + 10 * DAY_MS);
|
||||
const id = await seedItem(prisma, medicationId, {
|
||||
containerType: "BLISTER",
|
||||
state,
|
||||
unitsRemaining: state === "USED_UP" ? 0 : 54,
|
||||
expiresAt,
|
||||
expiresAt: new Date(Date.now() + 10 * DAY_MS),
|
||||
});
|
||||
|
||||
await prisma.$executeRawUnsafe(REPAIR_SQL);
|
||||
|
||||
const row = await readItem(prisma, id);
|
||||
expect(row.state).toBe(state);
|
||||
expect(row.expiresAt).toEqual(expiresAt);
|
||||
expect((await readItem(prisma, id)).state).toBe(state);
|
||||
},
|
||||
);
|
||||
|
||||
it("clears the pending clock deadline on a still-IN_USE blister", async () => {
|
||||
// The rows the flip has not reached yet. Their `expires_at` is the
|
||||
// old firstUseAt + 30 days, still in the future, and the daily
|
||||
// expire scan would have written them off on schedule.
|
||||
const prisma = getPrismaClient();
|
||||
const openedAt = new Date(Date.now() - 10 * DAY_MS);
|
||||
const id = await seedItem(prisma, medicationId, {
|
||||
containerType: "BLISTER",
|
||||
state: "IN_USE",
|
||||
firstUseAt: openedAt,
|
||||
printedExpiry: null,
|
||||
expiresAt: new Date(openedAt.getTime() + 30 * DAY_MS),
|
||||
});
|
||||
|
||||
await prisma.$executeRawUnsafe(REPAIR_SQL);
|
||||
|
||||
const row = await readItem(prisma, id);
|
||||
expect(row.state).toBe("IN_USE");
|
||||
expect(row.expiresAt).toBeNull();
|
||||
});
|
||||
|
||||
it("leaves a pen's pending clock deadline in place", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const openedAt = new Date(Date.now() - 10 * DAY_MS);
|
||||
const expiresAt = new Date(openedAt.getTime() + 30 * DAY_MS);
|
||||
const id = await seedItem(prisma, medicationId, {
|
||||
containerType: "PEN",
|
||||
state: "IN_USE",
|
||||
firstUseAt: openedAt,
|
||||
printedExpiry: null,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
await prisma.$executeRawUnsafe(REPAIR_SQL);
|
||||
|
||||
const row = await readItem(prisma, id);
|
||||
expect(row.state).toBe("IN_USE");
|
||||
expect(row.expiresAt).toEqual(expiresAt);
|
||||
});
|
||||
|
||||
it("is idempotent — a second application changes nothing", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const id = await seedItem(prisma, medicationId, {
|
||||
|
||||
Reference in new issue
Block a user