mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
fix(dashboard,medications): review findings before the cut
A fresh crisis-level blood-pressure reading outranks an overdue dose in the daily verdict, and once it ages past a day the dose leads again. The greeting returns to the page header whenever the daily band is hidden, and the settings toggle says it rides along. The nightly convergence pass honours the early grace, so a take recorded shortly before a window that starts at the dose time heals like the write path credits it. The early-take context line states a fact — last dose yesterday, last dose n days ago — instead of claiming an early intake after a late one, the quick-add default pick runs in the profile timezone, a dose line without a medication name reads cleanly, the early-grace constant has one home, and the Telegram confirm writes intake and snooze atomically again.
This commit is contained in:
1 parent
aaecd141ee
commit
b4d2d51d46
29 files changed
+601
-210
No files matched your search
@@ -137,7 +137,13 @@ beforeEach(() => {
|
||||
vi.mocked(prisma.telegramScheduledDeletion.createMany).mockResolvedValue({
|
||||
count: 0,
|
||||
} as never);
|
||||
vi.mocked(prisma.$transaction).mockResolvedValue([] as never);
|
||||
// Interactive transactions run their callback against the same mock
|
||||
// client (the route converges the intake write + snooze update inside
|
||||
// one transaction); batch arrays resolve like Promise.all.
|
||||
vi.mocked(prisma.$transaction).mockImplementation(((arg: unknown) =>
|
||||
typeof arg === "function"
|
||||
? (arg as (tx: unknown) => unknown)(prisma)
|
||||
: Promise.all(arg as Promise<unknown>[])) as never);
|
||||
vi.mocked(answerTelegramCallbackQuery).mockResolvedValue(undefined as never);
|
||||
vi.mocked(deleteMessage).mockResolvedValue(undefined as never);
|
||||
vi.mocked(sendTelegramMessage).mockResolvedValue({
|
||||
@@ -249,6 +255,8 @@ describe("Telegram webhook — callback dispatch", () => {
|
||||
where: { id: "med-1" },
|
||||
data: { snoozedUntil: null },
|
||||
});
|
||||
// Intake write + snooze reset commit atomically.
|
||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(answerTelegramCallbackQuery).toHaveBeenCalledTimes(1);
|
||||
expect(deleteMessage).toHaveBeenCalledWith(
|
||||
@@ -319,6 +327,8 @@ describe("Telegram webhook — callback dispatch", () => {
|
||||
.snoozedUntil;
|
||||
expect(snoozedUntil.getHours()).toBe(23);
|
||||
expect(snoozedUntil.getMinutes()).toBe(59);
|
||||
// Skip row + rest-of-day snooze commit atomically.
|
||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("'ack:<medId>' answers the callback without creating an intake event", async () => {
|
||||
|
||||
@@ -259,40 +259,53 @@ async function markMedicationTaken(
|
||||
canonicalSlot = attribution.slotInstant;
|
||||
}
|
||||
|
||||
if (canonicalSlot) {
|
||||
const applied = await applyCanonicalSlotWrite({
|
||||
client: prisma,
|
||||
userId,
|
||||
medicationId: medication.id,
|
||||
canonicalSlot,
|
||||
takenAt,
|
||||
skipped: false,
|
||||
isExplicitTaken: true,
|
||||
isExplicitSkip: false,
|
||||
idempotencyKey,
|
||||
createSource: "REMINDER",
|
||||
attributionSource: "AUTO",
|
||||
});
|
||||
scheduledFor = applied.row.scheduledFor;
|
||||
} else {
|
||||
// Ad-hoc / PRN — standalone row under the documented contract
|
||||
// (`scheduledFor = takenAt`).
|
||||
const created = await prisma.medicationIntakeEvent.create({
|
||||
data: {
|
||||
// The intake write and the snooze reset commit atomically — a
|
||||
// recorded dose must never leave the medication snoozed-out of its
|
||||
// own reminders, and a snooze reset must never land without the dose
|
||||
// (the pre-band shape used a batch transaction here; the slot
|
||||
// convergence dropped it). The canonical slot write accepts the
|
||||
// transaction client; its P2002 race-recovery cannot continue inside
|
||||
// an aborted Postgres transaction, so the rare create race rolls the
|
||||
// whole write back instead of converging in place — Telegram
|
||||
// redelivers the non-200 update and the retry finds the raced row.
|
||||
scheduledFor = await prisma.$transaction(async (tx) => {
|
||||
let slotScheduledFor: Date;
|
||||
if (canonicalSlot) {
|
||||
const applied = await applyCanonicalSlotWrite({
|
||||
client: tx,
|
||||
userId,
|
||||
medicationId: medication.id,
|
||||
scheduledFor: takenAt,
|
||||
canonicalSlot,
|
||||
takenAt,
|
||||
skipped: false,
|
||||
source: "REMINDER",
|
||||
isExplicitTaken: true,
|
||||
isExplicitSkip: false,
|
||||
idempotencyKey,
|
||||
},
|
||||
createSource: "REMINDER",
|
||||
attributionSource: "AUTO",
|
||||
});
|
||||
slotScheduledFor = applied.row.scheduledFor;
|
||||
} else {
|
||||
// Ad-hoc / PRN — standalone row under the documented contract
|
||||
// (`scheduledFor = takenAt`).
|
||||
const created = await tx.medicationIntakeEvent.create({
|
||||
data: {
|
||||
userId,
|
||||
medicationId: medication.id,
|
||||
scheduledFor: takenAt,
|
||||
takenAt,
|
||||
skipped: false,
|
||||
source: "REMINDER",
|
||||
idempotencyKey,
|
||||
},
|
||||
});
|
||||
slotScheduledFor = created.scheduledFor;
|
||||
}
|
||||
await tx.medication.update({
|
||||
where: { id: medication.id },
|
||||
data: { snoozedUntil: null },
|
||||
});
|
||||
scheduledFor = created.scheduledFor;
|
||||
}
|
||||
await prisma.medication.update({
|
||||
where: { id: medication.id },
|
||||
data: { snoozedUntil: null },
|
||||
return slotScheduledFor;
|
||||
});
|
||||
// The confirmation must reflect on the next read (today feed, card
|
||||
// pill, compliance) rather than wait out the cache TTL.
|
||||
@@ -476,37 +489,46 @@ async function handleCallback(update: TelegramUpdate) {
|
||||
canonicalSlot = attribution.slotInstant;
|
||||
}
|
||||
|
||||
if (canonicalSlot) {
|
||||
const applied = await applyCanonicalSlotWrite({
|
||||
client: prisma,
|
||||
userId: user.id,
|
||||
medicationId: medication.id,
|
||||
canonicalSlot,
|
||||
takenAt: null,
|
||||
skipped: true,
|
||||
isExplicitTaken: false,
|
||||
isExplicitSkip: true,
|
||||
idempotencyKey,
|
||||
createSource: "REMINDER",
|
||||
});
|
||||
skippedScheduledFor = applied.row.scheduledFor;
|
||||
} else {
|
||||
const created = await prisma.medicationIntakeEvent.create({
|
||||
data: {
|
||||
// Atomic like the take path: the skip row and the rest-of-day
|
||||
// snooze commit together, so a failure can never leave the user
|
||||
// re-reminded for a recorded skip or silenced without one. Same
|
||||
// P2002 trade-off as the take path — the rare create race rolls
|
||||
// back and Telegram's redelivery converges on the raced row.
|
||||
skippedScheduledFor = await prisma.$transaction(async (tx) => {
|
||||
let slotScheduledFor: Date;
|
||||
if (canonicalSlot) {
|
||||
const applied = await applyCanonicalSlotWrite({
|
||||
client: tx,
|
||||
userId: user.id,
|
||||
medicationId: medication.id,
|
||||
scheduledFor: skipMoment,
|
||||
canonicalSlot,
|
||||
takenAt: null,
|
||||
skipped: true,
|
||||
source: "REMINDER",
|
||||
isExplicitTaken: false,
|
||||
isExplicitSkip: true,
|
||||
idempotencyKey,
|
||||
},
|
||||
createSource: "REMINDER",
|
||||
});
|
||||
slotScheduledFor = applied.row.scheduledFor;
|
||||
} else {
|
||||
const created = await tx.medicationIntakeEvent.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
medicationId: medication.id,
|
||||
scheduledFor: skipMoment,
|
||||
takenAt: null,
|
||||
skipped: true,
|
||||
source: "REMINDER",
|
||||
idempotencyKey,
|
||||
},
|
||||
});
|
||||
slotScheduledFor = created.scheduledFor;
|
||||
}
|
||||
await tx.medication.update({
|
||||
where: { id: medication.id },
|
||||
data: { snoozedUntil: endOfDay },
|
||||
});
|
||||
skippedScheduledFor = created.scheduledFor;
|
||||
}
|
||||
await prisma.medication.update({
|
||||
where: { id: medication.id },
|
||||
data: { snoozedUntil: endOfDay },
|
||||
return slotScheduledFor;
|
||||
});
|
||||
invalidateUserMedications(user.id, { evict: true });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user