mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
feat: scope Telegram reminder tracking to recipients
- keep delegated recipient slots independent\n- bind callback cleanup to the recipient account
This commit is contained in:
1 parent
5807879439
commit
fa46f27b01
6 files changed
+99
-51
No files matched your search
@@ -50,6 +50,34 @@ CREATE TABLE "notification_events" (
|
|||||||
CREATE INDEX "notification_events_record_user_id_event_type_dedup_key_created_at_desc_idx"
|
CREATE INDEX "notification_events_record_user_id_event_type_dedup_key_created_at_desc_idx"
|
||||||
ON "notification_events" ("record_user_id", "event_type", "dedup_key", "created_at" DESC);
|
ON "notification_events" ("record_user_id", "event_type", "dedup_key", "created_at" DESC);
|
||||||
|
|
||||||
|
-- Tracking belongs to the account that received the Telegram message. The
|
||||||
|
-- prior subject-only key collapsed multiple delegated recipients into one row.
|
||||||
|
ALTER TABLE "telegram_reminder_messages"
|
||||||
|
ADD COLUMN "recipient_user_id" TEXT;
|
||||||
|
|
||||||
|
UPDATE "telegram_reminder_messages" AS "message"
|
||||||
|
SET "recipient_user_id" = "medication"."user_id"
|
||||||
|
FROM "medications" AS "medication"
|
||||||
|
WHERE "medication"."id" = "message"."medication_id";
|
||||||
|
|
||||||
|
ALTER TABLE "telegram_reminder_messages"
|
||||||
|
ALTER COLUMN "recipient_user_id" SET NOT NULL,
|
||||||
|
ADD CONSTRAINT "telegram_reminder_messages_recipient_user_id_fkey"
|
||||||
|
FOREIGN KEY ("recipient_user_id") REFERENCES "users"("id")
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
DROP INDEX "telegram_reminder_messages_med_sched_date_phase_tod_key";
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "telegram_reminder_messages_recipient_slot_key"
|
||||||
|
ON "telegram_reminder_messages" (
|
||||||
|
"recipient_user_id",
|
||||||
|
"medication_id",
|
||||||
|
"schedule_id",
|
||||||
|
"date",
|
||||||
|
"phase",
|
||||||
|
"time_of_day"
|
||||||
|
);
|
||||||
|
|
||||||
CREATE FUNCTION enforce_push_attempt_attribution()
|
CREATE FUNCTION enforce_push_attempt_attribution()
|
||||||
RETURNS TRIGGER
|
RETURNS TRIGGER
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
|
|||||||
+15
-12
@@ -863,6 +863,7 @@ model User {
|
|||||||
pushAttemptsForRecord PushAttempt[] @relation("PushAttemptRecordUser")
|
pushAttemptsForRecord PushAttempt[] @relation("PushAttemptRecordUser")
|
||||||
pushAttemptsReceived PushAttempt[] @relation("PushAttemptRecipientUser")
|
pushAttemptsReceived PushAttempt[] @relation("PushAttemptRecipientUser")
|
||||||
notificationEvents NotificationEvent[]
|
notificationEvents NotificationEvent[]
|
||||||
|
telegramReminderMessages TelegramReminderMessage[]
|
||||||
// v1.10.3 — per-(user, day) Strain personal-anchor cache (day-total
|
// v1.10.3 — per-(user, day) Strain personal-anchor cache (day-total
|
||||||
// TRIMP + running EWMA reference) so the nightly Strain engine anchors
|
// TRIMP + running EWMA reference) so the nightly Strain engine anchors
|
||||||
// the 0–100 map to the user's own training load without re-integrating
|
// the 0–100 map to the user's own training load without re-integrating
|
||||||
@@ -3364,24 +3365,26 @@ model ReminderPhaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model TelegramReminderMessage {
|
model TelegramReminderMessage {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
medicationId String @map("medication_id")
|
recipientUserId String @map("recipient_user_id")
|
||||||
scheduleId String @map("schedule_id")
|
medicationId String @map("medication_id")
|
||||||
chatId String @map("chat_id")
|
scheduleId String @map("schedule_id")
|
||||||
messageId Int @map("message_id")
|
chatId String @map("chat_id")
|
||||||
phase ReminderPhase
|
messageId Int @map("message_id")
|
||||||
sentAt DateTime @default(now()) @map("sent_at")
|
phase ReminderPhase
|
||||||
date String // YYYY-MM-DD for day grouping
|
sentAt DateTime @default(now()) @map("sent_at")
|
||||||
|
date String // YYYY-MM-DD for day grouping
|
||||||
/// v1.7.0 — time-of-day this reminder belongs to (HH:mm, user-local).
|
/// v1.7.0 — time-of-day this reminder belongs to (HH:mm, user-local).
|
||||||
/// Empty string = the single legacy window (pre-v1.7 rows + single-time
|
/// Empty string = the single legacy window (pre-v1.7 rows + single-time
|
||||||
/// schedules). Part of the dedup key so a multi-time schedule dispatches
|
/// schedules). Part of the dedup key so a multi-time schedule dispatches
|
||||||
/// once per time-of-day per phase per day instead of once per day.
|
/// once per time-of-day per phase per day instead of once per day.
|
||||||
timeOfDay String @default("") @map("time_of_day")
|
timeOfDay String @default("") @map("time_of_day")
|
||||||
|
|
||||||
medication Medication @relation(fields: [medicationId], references: [id], onDelete: Cascade)
|
recipientUser User @relation(fields: [recipientUserId], references: [id], onDelete: Cascade)
|
||||||
schedule MedicationSchedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)
|
medication Medication @relation(fields: [medicationId], references: [id], onDelete: Cascade)
|
||||||
|
schedule MedicationSchedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([medicationId, scheduleId, date, phase, timeOfDay], map: "telegram_reminder_messages_med_sched_date_phase_tod_key")
|
@@unique([recipientUserId, medicationId, scheduleId, date, phase, timeOfDay], map: "telegram_reminder_messages_recipient_slot_key")
|
||||||
@@index([medicationId, date])
|
@@index([medicationId, date])
|
||||||
@@map("telegram_reminder_messages")
|
@@map("telegram_reminder_messages")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,14 @@ describe("Telegram webhook — callback dispatch", () => {
|
|||||||
const res = await POST(tgRequest(callbackUpdate("taken:med-1")));
|
const res = await POST(tgRequest(callbackUpdate("taken:med-1")));
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
expect(prisma.telegramReminderMessage.deleteMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
recipientUserId: "user-1",
|
||||||
|
medicationId: "med-1",
|
||||||
|
medication: { userId: "user-1" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// No schedules wired → the take is ad-hoc and inserts standalone.
|
// No schedules wired → the take is ad-hoc and inserts standalone.
|
||||||
expect(prisma.medicationIntakeEvent.create).toHaveBeenCalledTimes(1);
|
expect(prisma.medicationIntakeEvent.create).toHaveBeenCalledTimes(1);
|
||||||
const intakeArgs = vi.mocked(prisma.medicationIntakeEvent.create).mock
|
const intakeArgs = vi.mocked(prisma.medicationIntakeEvent.create).mock
|
||||||
@@ -779,6 +787,16 @@ describe("Telegram webhook — slot convergence (v1.16.9)", () => {
|
|||||||
const res = await POST(tgRequest(callbackUpdate("taken:med-1")));
|
const res = await POST(tgRequest(callbackUpdate("taken:med-1")));
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
expect(prisma.telegramReminderMessage.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
recipientUserId: "user-1",
|
||||||
|
medicationId: "med-1",
|
||||||
|
chatId: "7777",
|
||||||
|
messageId: 555,
|
||||||
|
},
|
||||||
|
select: { date: true, timeOfDay: true },
|
||||||
|
});
|
||||||
|
|
||||||
// The pending row was updated in place — never a duplicate insert.
|
// The pending row was updated in place — never a duplicate insert.
|
||||||
expect(prisma.medicationIntakeEvent.create).not.toHaveBeenCalled();
|
expect(prisma.medicationIntakeEvent.create).not.toHaveBeenCalled();
|
||||||
expect(prisma.medicationIntakeEvent.update).toHaveBeenCalledTimes(1);
|
expect(prisma.medicationIntakeEvent.update).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
const sendTelegramMessageMock = vi.fn();
|
const sendTelegramMessageMock = vi.fn();
|
||||||
const deleteMessageMock = vi.fn();
|
const deleteMessageMock = vi.fn();
|
||||||
const findUniqueMock = vi.fn();
|
const findUniqueMock = vi.fn();
|
||||||
const deleteMock = vi.fn();
|
|
||||||
const upsertMock = vi.fn();
|
const upsertMock = vi.fn();
|
||||||
const userFindUniqueMock = vi.fn();
|
const userFindUniqueMock = vi.fn();
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ vi.mock("@/lib/db", () => ({
|
|||||||
},
|
},
|
||||||
telegramReminderMessage: {
|
telegramReminderMessage: {
|
||||||
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
||||||
delete: (...args: unknown[]) => deleteMock(...args),
|
|
||||||
upsert: (...args: unknown[]) => upsertMock(...args),
|
upsert: (...args: unknown[]) => upsertMock(...args),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -79,7 +77,6 @@ describe("sendViaTelegram — per-slot delete scope (H2)", () => {
|
|||||||
sendTelegramMessageMock.mockResolvedValue({ ok: true, messageId: 999 });
|
sendTelegramMessageMock.mockResolvedValue({ ok: true, messageId: 999 });
|
||||||
deleteMessageMock.mockResolvedValue(undefined);
|
deleteMessageMock.mockResolvedValue(undefined);
|
||||||
findUniqueMock.mockResolvedValue(null);
|
findUniqueMock.mockResolvedValue(null);
|
||||||
deleteMock.mockResolvedValue(undefined);
|
|
||||||
upsertMock.mockResolvedValue(undefined);
|
upsertMock.mockResolvedValue(undefined);
|
||||||
userFindUniqueMock.mockResolvedValue({ locale: "en" });
|
userFindUniqueMock.mockResolvedValue({ locale: "en" });
|
||||||
scheduleTelegramAutoDeleteMock.mockResolvedValue(undefined);
|
scheduleTelegramAutoDeleteMock.mockResolvedValue(undefined);
|
||||||
@@ -107,7 +104,7 @@ describe("sendViaTelegram — per-slot delete scope (H2)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deletes only the matching slot's ledger row, leaving sibling slots untouched", async () => {
|
it("replaces only the matching slot's remote message, leaving its tracking row for update", async () => {
|
||||||
// The 20:00 slot has a prior row; the lookup returns it.
|
// The 20:00 slot has a prior row; the lookup returns it.
|
||||||
findUniqueMock.mockResolvedValue({
|
findUniqueMock.mockResolvedValue({
|
||||||
chatId: "chat-1",
|
chatId: "chat-1",
|
||||||
@@ -118,20 +115,22 @@ describe("sendViaTelegram — per-slot delete scope (H2)", () => {
|
|||||||
|
|
||||||
// The Telegram message for the old 20:00 row is deleted...
|
// The Telegram message for the old 20:00 row is deleted...
|
||||||
expect(deleteMessageMock).toHaveBeenCalledWith("bot-token", "chat-1", 123);
|
expect(deleteMessageMock).toHaveBeenCalledWith("bot-token", "chat-1", 123);
|
||||||
// ...and exactly that single ledger row is removed (scoped delete, not deleteMany).
|
// The tracking row stays in place so the recipient-scoped upsert updates
|
||||||
expect(deleteMock).toHaveBeenCalledTimes(1);
|
// it rather than creating a second row when the chat binding changes.
|
||||||
expect(deleteMock).toHaveBeenCalledWith({
|
expect(upsertMock).toHaveBeenCalledWith(
|
||||||
where: {
|
expect.objectContaining({
|
||||||
recipientUserId_medicationId_scheduleId_date_phase_timeOfDay: {
|
where: {
|
||||||
recipientUserId: "user-1",
|
recipientUserId_medicationId_scheduleId_date_phase_timeOfDay: {
|
||||||
medicationId: "med-1",
|
recipientUserId: "user-1",
|
||||||
scheduleId: "sched-1",
|
medicationId: "med-1",
|
||||||
date: "2025-06-10",
|
scheduleId: "sched-1",
|
||||||
phase: "YELLOW",
|
date: "2025-06-10",
|
||||||
timeOfDay: "20:00",
|
phase: "YELLOW",
|
||||||
|
timeOfDay: "20:00",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not delete anything when no prior row exists for the slot", async () => {
|
it("does not delete anything when no prior row exists for the slot", async () => {
|
||||||
@@ -140,7 +139,6 @@ describe("sendViaTelegram — per-slot delete scope (H2)", () => {
|
|||||||
await sendViaTelegram(config, reminderPayload());
|
await sendViaTelegram(config, reminderPayload());
|
||||||
|
|
||||||
expect(deleteMessageMock).not.toHaveBeenCalled();
|
expect(deleteMessageMock).not.toHaveBeenCalled();
|
||||||
expect(deleteMock).not.toHaveBeenCalled();
|
|
||||||
// It still sends + tracks the new message.
|
// It still sends + tracks the new message.
|
||||||
expect(sendTelegramMessageMock).toHaveBeenCalledTimes(1);
|
expect(sendTelegramMessageMock).toHaveBeenCalledTimes(1);
|
||||||
expect(upsertMock).toHaveBeenCalledTimes(1);
|
expect(upsertMock).toHaveBeenCalledTimes(1);
|
||||||
@@ -155,7 +153,6 @@ describe("sendViaTelegram — per-slot delete scope (H2)", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(findUniqueMock).not.toHaveBeenCalled();
|
expect(findUniqueMock).not.toHaveBeenCalled();
|
||||||
expect(deleteMock).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps two recipients' identical subject slots independent", async () => {
|
it("keeps two recipients' identical subject slots independent", async () => {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export interface TelegramSendResult extends SendOutcome {
|
|||||||
/**
|
/**
|
||||||
* Delete the existing Telegram reminder message for a single dose slot
|
* Delete the existing Telegram reminder message for a single dose slot
|
||||||
* before re-sending it. v1.7.0 SB-SCHED-4 — scoped to the exact slot
|
* before re-sending it. v1.7.0 SB-SCHED-4 — scoped to the exact slot
|
||||||
* `{ medicationId, scheduleId, date, phase, timeOfDay }` (the same
|
* `{ recipientUserId, medicationId, scheduleId, date, phase, timeOfDay }` (the same
|
||||||
* composite the tracking upsert keys on), NOT the whole medication. A
|
* composite the tracking upsert keys on), NOT the whole medication. A
|
||||||
* medication with `timesOfDay = ["08:00","20:00"]` has two distinct
|
* medication with `timesOfDay = ["08:00","20:00"]` has two distinct
|
||||||
* ledger rows per day; the pre-v1.7 whole-medication wipe deleted the
|
* ledger rows per day; the pre-v1.7 whole-medication wipe deleted the
|
||||||
@@ -62,6 +62,7 @@ export interface TelegramSendResult extends SendOutcome {
|
|||||||
*/
|
*/
|
||||||
async function deleteExistingReminders(
|
async function deleteExistingReminders(
|
||||||
botToken: string,
|
botToken: string,
|
||||||
|
recipientUserId: string,
|
||||||
medicationId: string,
|
medicationId: string,
|
||||||
scheduleId: string,
|
scheduleId: string,
|
||||||
date: string,
|
date: string,
|
||||||
@@ -71,7 +72,8 @@ async function deleteExistingReminders(
|
|||||||
try {
|
try {
|
||||||
const existing = await prisma.telegramReminderMessage.findUnique({
|
const existing = await prisma.telegramReminderMessage.findUnique({
|
||||||
where: {
|
where: {
|
||||||
medicationId_scheduleId_date_phase_timeOfDay: {
|
recipientUserId_medicationId_scheduleId_date_phase_timeOfDay: {
|
||||||
|
recipientUserId,
|
||||||
medicationId,
|
medicationId,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
date,
|
date,
|
||||||
@@ -89,17 +91,6 @@ async function deleteExistingReminders(
|
|||||||
// Best-effort: message may already be deleted
|
// Best-effort: message may already be deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.telegramReminderMessage.delete({
|
|
||||||
where: {
|
|
||||||
medicationId_scheduleId_date_phase_timeOfDay: {
|
|
||||||
medicationId,
|
|
||||||
scheduleId,
|
|
||||||
date,
|
|
||||||
phase,
|
|
||||||
timeOfDay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
getEvent()?.addWarning(`Failed to delete existing reminders: ${err}`);
|
getEvent()?.addWarning(`Failed to delete existing reminders: ${err}`);
|
||||||
}
|
}
|
||||||
@@ -136,6 +127,7 @@ export async function sendViaTelegram(
|
|||||||
if (medicationId && scheduleId && phase && date) {
|
if (medicationId && scheduleId && phase && date) {
|
||||||
await deleteExistingReminders(
|
await deleteExistingReminders(
|
||||||
config.botToken,
|
config.botToken,
|
||||||
|
userId,
|
||||||
medicationId,
|
medicationId,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
date,
|
date,
|
||||||
@@ -220,7 +212,8 @@ export async function sendViaTelegram(
|
|||||||
try {
|
try {
|
||||||
await prisma.telegramReminderMessage.upsert({
|
await prisma.telegramReminderMessage.upsert({
|
||||||
where: {
|
where: {
|
||||||
medicationId_scheduleId_date_phase_timeOfDay: {
|
recipientUserId_medicationId_scheduleId_date_phase_timeOfDay: {
|
||||||
|
recipientUserId: userId,
|
||||||
medicationId,
|
medicationId,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
date,
|
date,
|
||||||
@@ -229,6 +222,7 @@ export async function sendViaTelegram(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
|
recipientUserId: userId,
|
||||||
medicationId,
|
medicationId,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
chatId: config.chatId,
|
chatId: config.chatId,
|
||||||
|
|||||||
@@ -90,10 +90,17 @@ function resolveBotLocale(value: string | null | undefined): Locale {
|
|||||||
return "de";
|
return "de";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupReminderTracking(medicationId: string): Promise<void> {
|
async function cleanupReminderTracking(
|
||||||
|
recipientUserId: string,
|
||||||
|
medicationId: string,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await prisma.telegramReminderMessage.deleteMany({
|
await prisma.telegramReminderMessage.deleteMany({
|
||||||
where: { medicationId },
|
where: {
|
||||||
|
recipientUserId,
|
||||||
|
medicationId,
|
||||||
|
medication: { userId: recipientUserId },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Best-effort cleanup
|
// Best-effort cleanup
|
||||||
@@ -153,6 +160,7 @@ async function resolveReminderSlot(input: {
|
|||||||
}): Promise<Date | null> {
|
}): Promise<Date | null> {
|
||||||
const reminder = await prisma.telegramReminderMessage.findFirst({
|
const reminder = await prisma.telegramReminderMessage.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
recipientUserId: input.userId,
|
||||||
medicationId: input.medicationId,
|
medicationId: input.medicationId,
|
||||||
chatId: input.chatId,
|
chatId: input.chatId,
|
||||||
messageId: input.messageId,
|
messageId: input.messageId,
|
||||||
@@ -398,7 +406,7 @@ export async function handleCallback(update: TelegramUpdate) {
|
|||||||
if (messageId) {
|
if (messageId) {
|
||||||
await deleteMessage(botToken, chatId, messageId);
|
await deleteMessage(botToken, chatId, messageId);
|
||||||
}
|
}
|
||||||
await cleanupReminderTracking(medicationId);
|
await cleanupReminderTracking(user.id, medicationId);
|
||||||
} else if (data.startsWith("snooze:")) {
|
} else if (data.startsWith("snooze:")) {
|
||||||
// Format: "snooze:{medicationId}:{minutes}"
|
// Format: "snooze:{medicationId}:{minutes}"
|
||||||
const parts = data.split(":");
|
const parts = data.split(":");
|
||||||
@@ -443,7 +451,7 @@ export async function handleCallback(update: TelegramUpdate) {
|
|||||||
if (messageId) {
|
if (messageId) {
|
||||||
await deleteMessage(botToken, chatId, messageId);
|
await deleteMessage(botToken, chatId, messageId);
|
||||||
}
|
}
|
||||||
await cleanupReminderTracking(medicationId);
|
await cleanupReminderTracking(user.id, medicationId);
|
||||||
} else if (data.startsWith("skip:")) {
|
} else if (data.startsWith("skip:")) {
|
||||||
const medicationId = data.slice("skip:".length).trim();
|
const medicationId = data.slice("skip:".length).trim();
|
||||||
if (!medicationId) {
|
if (!medicationId) {
|
||||||
@@ -586,7 +594,7 @@ export async function handleCallback(update: TelegramUpdate) {
|
|||||||
if (messageId) {
|
if (messageId) {
|
||||||
await deleteMessage(botToken, chatId, messageId);
|
await deleteMessage(botToken, chatId, messageId);
|
||||||
}
|
}
|
||||||
await cleanupReminderTracking(medicationId);
|
await cleanupReminderTracking(user.id, medicationId);
|
||||||
} else if (data.startsWith("ack:")) {
|
} else if (data.startsWith("ack:")) {
|
||||||
const medicationId = data.slice("ack:".length).trim();
|
const medicationId = data.slice("ack:".length).trim();
|
||||||
if (!medicationId) {
|
if (!medicationId) {
|
||||||
@@ -613,7 +621,7 @@ export async function handleCallback(update: TelegramUpdate) {
|
|||||||
if (messageId) {
|
if (messageId) {
|
||||||
await deleteMessage(botToken, chatId, messageId);
|
await deleteMessage(botToken, chatId, messageId);
|
||||||
}
|
}
|
||||||
await cleanupReminderTracking(medicationId);
|
await cleanupReminderTracking(user.id, medicationId);
|
||||||
} else if (data.startsWith("add:")) {
|
} else if (data.startsWith("add:")) {
|
||||||
// Format: "add:{medicationId}" or "add:{medicationId}:umid:{userMsgId}"
|
// Format: "add:{medicationId}" or "add:{medicationId}:umid:{userMsgId}"
|
||||||
const withoutPrefix = data.slice("add:".length);
|
const withoutPrefix = data.slice("add:".length);
|
||||||
|
|||||||
Reference in new issue
Block a user