mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
/**
|
|
* v1.12.1 — partial live-row unique index on MedicationIntakeEvent
|
|
* (migration 0121).
|
|
*
|
|
* This pins the delete-then-resync contract on real Postgres, against the
|
|
* migrations replayed by `global-setup.ts`: the real re-take path through
|
|
* `applyCanonicalSlotWrite` takes a slot, soft-deletes it (user "forgot
|
|
* this"), then re-takes the SAME canonical slot. Pre-0121 the create P2002'd
|
|
* against the tombstone and the catch re-found only live rows (none) and
|
|
* re-threw — the re-take 500'd. With the partial live-row unique it re-creates
|
|
* cleanly.
|
|
*
|
|
* The MoodEntry half of this file drove migration 0122's external-id dedup key
|
|
* through the moodLog webhook. That bridge was retired in v1.32.33; the same
|
|
* index is pinned end-to-end through the surviving native write paths in
|
|
* `mood-entries-external-id.test.ts`.
|
|
*
|
|
* Note: Measurement is intentionally NOT made partial-unique (its
|
|
* compound-key writes use `prisma.upsert` → native ON CONFLICT, which
|
|
* Postgres cannot arbiter against a partial unique). See migration 0121's
|
|
* header for the full rationale.
|
|
*/
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
|
|
process.env.ENCRYPTION_KEY ??=
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
|
|
import { applyCanonicalSlotWrite } from "@/lib/medications/scheduling/slot-upsert";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
});
|
|
|
|
describe("MedicationIntakeEvent — re-take after delete (v1.12.1 / 0121)", () => {
|
|
it("re-takes a previously-deleted slot without 500-ing", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username: "partial-intake",
|
|
email: "partial-intake@example.test",
|
|
role: "USER",
|
|
},
|
|
});
|
|
const med = await prisma.medication.create({
|
|
data: { userId: user.id, name: "Ramipril", dose: "5mg" },
|
|
});
|
|
|
|
const slot = new Date("2026-06-03T06:00:00.000Z");
|
|
|
|
// First take — fresh slot.
|
|
const firstTake = await applyCanonicalSlotWrite({
|
|
client: prisma,
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
canonicalSlot: slot,
|
|
takenAt: new Date("2026-06-03T06:05:00.000Z"),
|
|
skipped: false,
|
|
isExplicitTaken: true,
|
|
isExplicitSkip: false,
|
|
idempotencyKey: null,
|
|
createSource: "WEB",
|
|
});
|
|
expect(firstTake.outcome).toBe("inserted");
|
|
expect(firstTake.row.takenAt).not.toBeNull();
|
|
|
|
// User deletes the take (soft-delete the slot row).
|
|
await prisma.medicationIntakeEvent.update({
|
|
where: { id: firstTake.row.id },
|
|
data: { deletedAt: new Date(), syncVersion: { increment: 1 } },
|
|
});
|
|
|
|
// Re-take the SAME canonical slot. Pre-0121 this threw the original
|
|
// P2002 (tombstone occupied (user, med, scheduled_for, source)).
|
|
const reTake = await applyCanonicalSlotWrite({
|
|
client: prisma,
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
canonicalSlot: slot,
|
|
takenAt: new Date("2026-06-03T06:10:00.000Z"),
|
|
skipped: false,
|
|
isExplicitTaken: true,
|
|
isExplicitSkip: false,
|
|
idempotencyKey: null,
|
|
createSource: "WEB",
|
|
});
|
|
expect(reTake.outcome).toBe("inserted");
|
|
expect(reTake.row.id).not.toBe(firstTake.row.id);
|
|
expect(reTake.row.takenAt).not.toBeNull();
|
|
|
|
// Exactly one LIVE slot row; the tombstone remains.
|
|
const live = await prisma.medicationIntakeEvent.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
scheduledFor: slot,
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
expect(live).toHaveLength(1);
|
|
expect(live[0].id).toBe(reTake.row.id);
|
|
|
|
// The live-row partial unique still forbids a SECOND live duplicate
|
|
// on the same (user, med, slot, source).
|
|
await expect(
|
|
prisma.medicationIntakeEvent.create({
|
|
data: {
|
|
userId: user.id,
|
|
medicationId: med.id,
|
|
scheduledFor: slot,
|
|
source: "WEB",
|
|
},
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|