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:
Marc-André Bombeck committed 2026-06-12 11:22:21 +02:00
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 () => {
+75 -53
View File
@@ -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 });
}