From c291c88f2b5de0be356416d268a5c6b22930097f Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Fri, 10 Apr 2026 22:34:06 +0200 Subject: [PATCH] fix(notifications): fallback to generic medication names (#532) * fix(notifications): fallback to generic medication names * test(backend): add timezone column to in-memory user_settings schemas --- .../src/services/intake-reminder-scheduler.ts | 15 ++++++++++--- backend/src/services/reminder-scheduler.ts | 14 ++++++++++-- .../src/test/stock-semantics-parity.test.ts | 22 +++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/backend/src/services/intake-reminder-scheduler.ts b/backend/src/services/intake-reminder-scheduler.ts index 5490f46..05e0c46 100644 --- a/backend/src/services/intake-reminder-scheduler.ts +++ b/backend/src/services/intake-reminder-scheduler.ts @@ -19,7 +19,6 @@ import { cleanOldIntakeReminders, createDefaultIntakeReminderState, getEffectiveTimezone, - getTimezone, getTodaysIntakes, getUpcomingIntakes, type IntakeReminderState, @@ -84,6 +83,16 @@ function formatIntakeLog(intake: { return `${intake.medName} (medId=${intake.medicationId}, intakeIndex=${intake.blisterIndex}, time=${intake.intakeTime.toISOString()}, localTime=${intake.intakeTimeStr}, usage=${intake.usage} ${doseUnit}, takenBy=${takenBy})`; } +function getMedicationDisplayName(med: { id: number; name: string | null; genericName: string | null }): string { + const commercialName = med.name?.trim() ?? ""; + if (commercialName) return commercialName; + + const genericName = med.genericName?.trim() ?? ""; + if (genericName) return genericName; + + return `Medication #${med.id}`; +} + async function autoMarkDueIntakesAsTaken( settings: UserSettings & { userId: number }, rows: (typeof medications.$inferSelect)[], @@ -138,7 +147,7 @@ async function autoMarkDueIntakesAsTaken( } const medicationTakenBy = parseTakenByJson(med.takenByJson); - const medDisplayName = med.name || med.genericName || ""; + const medDisplayName = getMedicationDisplayName({ id: med.id, name: med.name, genericName: med.genericName }); let remainingStock = computeMedicationCurrentStock({ medication: med, doses: trackedDoses, @@ -489,7 +498,7 @@ export async function checkAndSendIntakeRemindersForUser( for (const { med, intakes, intakesWithReminders } of reminderEntries) { // Medication-level takenBy (for fallback/display purposes) const medicationTakenBy = parseTakenByJson(med.takenByJson); - const medDisplayName = med.name || med.genericName || ""; + const medDisplayName = getMedicationDisplayName({ id: med.id, name: med.name, genericName: med.genericName }); // Process each intake separately to track blisterIndex intakesWithReminders.forEach((intake, _blisterIndex) => { diff --git a/backend/src/services/reminder-scheduler.ts b/backend/src/services/reminder-scheduler.ts index 0e8dbed..3dec954 100644 --- a/backend/src/services/reminder-scheduler.ts +++ b/backend/src/services/reminder-scheduler.ts @@ -126,6 +126,16 @@ type PrescriptionReminderItem = { expiryDate: string | null; }; +function getMedicationDisplayName(row: { id: number; name: string | null; genericName: string | null }): string { + const commercialName = row.name?.trim() ?? ""; + if (commercialName) return commercialName; + + const genericName = row.genericName?.trim() ?? ""; + if (genericName) return genericName; + + return `Medication #${row.id}`; +} + async function getMedicationsNeedingReminder( userId: number, reminderDaysBefore: number, @@ -297,7 +307,7 @@ async function getMedicationsNeedingReminder( if (isCritical || isLow) { lowStock.push({ - name: row.name, + name: getMedicationDisplayName({ id: row.id, name: row.name, genericName: row.genericName }), medsLeft: currentPills, daysLeft, depletionDate, @@ -323,7 +333,7 @@ async function getMedicationsNeedingPrescriptionReminder(userId: number): Promis (row.prescriptionRemainingRefills ?? 0) <= (row.prescriptionLowRefillThreshold ?? 1) ) .map((row) => ({ - name: row.name, + name: getMedicationDisplayName({ id: row.id, name: row.name, genericName: row.genericName }), remainingRefills: row.prescriptionRemainingRefills ?? 0, lowThreshold: row.prescriptionLowRefillThreshold ?? 1, expiryDate: row.prescriptionExpiryDate ?? null, diff --git a/backend/src/test/stock-semantics-parity.test.ts b/backend/src/test/stock-semantics-parity.test.ts index eb7e2c4..30cff28 100644 --- a/backend/src/test/stock-semantics-parity.test.ts +++ b/backend/src/test/stock-semantics-parity.test.ts @@ -68,6 +68,7 @@ async function setStockMode(mode: "automatic" | "manual") { async function createMedication(options: { name: string; + genericName?: string | null; packCount?: number; blistersPerPack?: number; pillsPerBlister?: number; @@ -80,6 +81,7 @@ async function createMedication(options: { }) { const { name, + genericName = null, packCount = 1, blistersPerPack = 1, pillsPerBlister = 10, @@ -106,16 +108,17 @@ async function createMedication(options: { const result = await testClient.execute({ sql: `INSERT INTO medications ( - user_id, name, taken_by_json, package_type, + user_id, name, generic_name, taken_by_json, package_type, pack_count, blisters_per_pack, pills_per_blister, loose_tablets, stock_adjustment, last_stock_correction_at, usage_json, every_json, start_json, intakes_json, is_obsolete, intake_reminders_enabled - ) VALUES (?, ?, ?, 'blister', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0) + ) VALUES (?, ?, ?, ?, 'blister', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0) RETURNING id`, args: [ 1, name, + genericName, JSON.stringify(takenBy), packCount, blistersPerPack, @@ -348,6 +351,21 @@ describe("Stock semantics parity (planner usage vs scheduler)", () => { const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic"); expect(lowStock.some((r) => r.name === "Obsolete Med")).toBe(false); }); + + it("uses generic name fallback in scheduler reminders when commercial name is empty", async () => { + await setStockMode("automatic"); + await createMedication({ + name: "", + genericName: "Acetylsalicylic acid", + packCount: 1, + blistersPerPack: 1, + pillsPerBlister: 10, + intakes: [{ usage: 1, every: 1, start: "2026-01-01T08:00:00" }], + }); + + const lowStock = await getMedicationsNeedingReminderForTests(1, 7, 365, "en", "automatic"); + expect(lowStock.some((r) => r.name === "Acetylsalicylic acid")).toBe(true); + }); }); describe("getLiquidReminderThresholds", () => {