mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(telegram): pick the running course and judge a reminder by its slot
The "taken <name>" keyword looked up the first medication of that name and only then asked whether its course had ended, so an old ended course beside the current one of the same name answered "not found". The lookup now reads every match and takes the first that runs. A reminder button judged the course at the moment of the tap, so the last evening dose of a course, confirmed after midnight, was refused although the web still takes it. When the message names its slot, the course is now judged at that slot. The handler file grows by six lines; its size baseline is raised to match.
This commit is contained in:
1 parent
d79060e26b
commit
df47980454
5 files changed
+100
-42
No files matched your search
@@ -132,7 +132,9 @@ const MEDICATION = { id: "med-1", name: "Ramipril" };
|
||||
* schedule projection (distinguishable by `select.schedules`); every other
|
||||
* `medication.findFirst` in the webhook wants the bare id/name shape. The
|
||||
* default projection carries no schedules, so the generic dispatch tests
|
||||
* exercise the ad-hoc (standalone create) path unchanged.
|
||||
* exercise the ad-hoc (standalone create) path unchanged. The dose lookup
|
||||
* (`findTelegramDoseMedication`) reads every match by id or name through
|
||||
* `findMany`; a list read (no id, no name) finds nothing unless a test says.
|
||||
*/
|
||||
function wireMedicationFindFirst(
|
||||
schedules: unknown[] = [],
|
||||
@@ -155,6 +157,14 @@ function wireMedicationFindFirst(
|
||||
}
|
||||
return Promise.resolve(medication);
|
||||
}) as never);
|
||||
vi.mocked(prisma.medication.findMany).mockImplementation(((args: {
|
||||
where?: Record<string, unknown>;
|
||||
}) =>
|
||||
Promise.resolve(
|
||||
args?.where && ("id" in args.where || "name" in args.where)
|
||||
? [medication]
|
||||
: [],
|
||||
)) as never);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -168,7 +178,6 @@ beforeEach(() => {
|
||||
vi.mocked(prisma.user.findFirst).mockResolvedValue(TG_USER as never);
|
||||
vi.mocked(isModuleEnabled).mockResolvedValue(true);
|
||||
wireMedicationFindFirst();
|
||||
vi.mocked(prisma.medication.findMany).mockResolvedValue([] as never);
|
||||
vi.mocked(prisma.medication.update).mockResolvedValue({} as never);
|
||||
vi.mocked(prisma.medicationIntakeEvent.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(prisma.medicationIntakeEvent.create).mockResolvedValue({} as never);
|
||||
@@ -305,6 +314,7 @@ describe("Telegram webhook — callback dispatch", () => {
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(prisma.medication.findFirst).not.toHaveBeenCalled();
|
||||
expect(prisma.medication.findMany).not.toHaveBeenCalled();
|
||||
expect(prisma.medicationIntakeEvent.create).not.toHaveBeenCalled();
|
||||
expect(prisma.medication.update).not.toHaveBeenCalled();
|
||||
expect(deleteMessage).not.toHaveBeenCalled();
|
||||
@@ -469,7 +479,7 @@ describe("Telegram webhook — callback dispatch", () => {
|
||||
});
|
||||
|
||||
it("rejects 'taken' for an inactive medication and acks with the localised error", async () => {
|
||||
vi.mocked(prisma.medication.findFirst).mockResolvedValueOnce(null);
|
||||
vi.mocked(prisma.medication.findMany).mockResolvedValueOnce([] as never);
|
||||
const res = await POST(tgRequest(callbackUpdate("taken:med-1")));
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
@@ -487,7 +497,7 @@ describe("Telegram webhook — callback dispatch", () => {
|
||||
});
|
||||
|
||||
it("'snooze' for a missing medication acks with medicationNotFound (no update)", async () => {
|
||||
vi.mocked(prisma.medication.findFirst).mockResolvedValueOnce(null);
|
||||
vi.mocked(prisma.medication.findMany).mockResolvedValueOnce([] as never);
|
||||
const res = await POST(tgRequest(callbackUpdate("snooze:med-X:60")));
|
||||
expect(res.status).toBe(200);
|
||||
expect(prisma.medication.update).not.toHaveBeenCalled();
|
||||
@@ -675,8 +685,8 @@ describe("Telegram webhook — text-message dispatch", () => {
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
// markMedicationTaken → findFirst on active medication
|
||||
expect(prisma.medication.findFirst).toHaveBeenCalled();
|
||||
// The list, then markMedicationTaken's lookup of the one it found.
|
||||
expect(prisma.medication.findMany).toHaveBeenCalledTimes(2);
|
||||
expect(prisma.medicationIntakeEvent.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -717,15 +727,6 @@ describe("Telegram webhook — text-message dispatch", () => {
|
||||
});
|
||||
|
||||
it("'taken Ramipril' (text command) creates an intake event for the matched medication", async () => {
|
||||
vi.mocked(prisma.medication.findFirst).mockResolvedValueOnce({
|
||||
id: "med-1",
|
||||
} as never);
|
||||
// Second findFirst inside markMedicationTaken
|
||||
vi.mocked(prisma.medication.findFirst).mockResolvedValueOnce({
|
||||
id: "med-1",
|
||||
name: "Ramipril",
|
||||
} as never);
|
||||
|
||||
const res = await POST(
|
||||
tgRequest({
|
||||
update_id: 206,
|
||||
@@ -767,8 +768,8 @@ describe("Telegram webhook — text-message dispatch", () => {
|
||||
});
|
||||
|
||||
it("'taken UnknownMed' replies with the medication-not-found message and writes nothing", async () => {
|
||||
// First findFirst (lookup by name) returns null
|
||||
vi.mocked(prisma.medication.findFirst).mockResolvedValueOnce(null);
|
||||
// The lookup by name finds nothing.
|
||||
vi.mocked(prisma.medication.findMany).mockResolvedValueOnce([] as never);
|
||||
|
||||
const res = await POST(
|
||||
tgRequest({
|
||||
@@ -912,6 +913,29 @@ describe("Telegram webhook — slot convergence (v1.16.9)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("books the reminder's own dose on the last day of a course after it ended", async () => {
|
||||
// The course ran through 9 June; yesterday's 09:00 reminder is confirmed
|
||||
// the next morning. The dose belongs to a day the course covered.
|
||||
wireMedicationFindFirst([scheduleRow], [], {
|
||||
...MEDICATION,
|
||||
dose: "5 mg",
|
||||
startsOn: null,
|
||||
endsOn: new Date("2026-06-09T00:00:00.000Z"),
|
||||
endedAt: null,
|
||||
});
|
||||
vi.mocked(prisma.telegramReminderMessage.findFirst).mockResolvedValue({
|
||||
date: "2026-06-09",
|
||||
timeOfDay: "09:00",
|
||||
} as never);
|
||||
|
||||
await POST(tgRequest(callbackUpdate("taken:med-1")));
|
||||
|
||||
expect(prisma.$transaction).toHaveBeenCalled();
|
||||
expect(vi.mocked(answerTelegramCallbackQuery).mock.calls[0][2]).not.toMatch(
|
||||
/not found or inactive/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("a take converges via band attribution when no reminder row matches", async () => {
|
||||
vi.mocked(prisma.telegramReminderMessage.findFirst).mockResolvedValue(null);
|
||||
const res = await POST(tgRequest(callbackUpdate("taken:med-1")));
|
||||
@@ -1224,7 +1248,7 @@ describe("POST /api/telegram/webhook — record-only medications", () => {
|
||||
|
||||
it("the 'taken' keyword resolves tracked medications only", async () => {
|
||||
await POST(tgRequest(textUpdate("taken Ramipril")));
|
||||
const where = whereOf(vi.mocked(prisma.medication.findFirst).mock.calls[0]);
|
||||
const where = whereOf(vi.mocked(prisma.medication.findMany).mock.calls[0]);
|
||||
expect(where).toMatchObject({ active: true, trackIntake: true });
|
||||
});
|
||||
|
||||
@@ -1232,7 +1256,7 @@ describe("POST /api/telegram/webhook — record-only medications", () => {
|
||||
await POST(tgRequest(callbackUpdate("taken:med-1")));
|
||||
// The gate lookup: a record-only medication resolves to nothing here,
|
||||
// so no dose is written (later lookups only bind an admitted dose).
|
||||
const calls = vi.mocked(prisma.medication.findFirst).mock.calls;
|
||||
const calls = vi.mocked(prisma.medication.findMany).mock.calls;
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
expect(whereOf(calls[0])).toMatchObject({
|
||||
id: "med-1",
|
||||
|
||||
Reference in new issue
Block a user