mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
feat(notifications): MEASUREMENT_REMINDER event type and client-managed gate
Register the MEASUREMENT_REMINDER event type (default on at the channel layer; the real gate is the per-reminder enabled flag) and add the notificationPrefs.measurementReminder.clientManaged sub-object plus its cron-side resolver, mirroring the medication and cycle client-managed precedent. The event rides the generic APNs category path and stays non-time-sensitive, so it delivers as a plain banner with no client change.
This commit is contained in:
1 parent
383963d3b4
commit
8058d81590
4 files changed
+152
No files matched your search
@@ -103,6 +103,9 @@ describe("GET /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,6 +138,9 @@ describe("GET /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,6 +175,9 @@ describe("GET /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -214,6 +223,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
||||
@@ -235,6 +247,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -260,6 +275,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
},
|
||||
next: {
|
||||
medication: {
|
||||
@@ -277,6 +295,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
},
|
||||
changed: ["medication"],
|
||||
}),
|
||||
@@ -347,6 +368,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -382,6 +406,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
||||
@@ -403,6 +430,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -443,6 +473,9 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,6 +56,16 @@ export const EVENT_TYPES = [
|
||||
// refill or threshold change. ON at the channel layer — the real
|
||||
// gate is the per-user threshold in `notificationPrefs.medication`.
|
||||
"MEDICATION_LOW_STOCK",
|
||||
// v1.17.1 — Vorsorge (measurement / preventive-care) reminder. Fired by
|
||||
// the every-15-min measurement-reminder cron when a user-created
|
||||
// reminder ("measure BP every 7 days", "annual blood panel") is due
|
||||
// inside the user's local notify-hour window. ON by default at the
|
||||
// channel layer (see EVENT_DEFAULT_ENABLED): these are reminders the
|
||||
// user explicitly created, so silence-by-default would be surprising.
|
||||
// The real gate is the per-reminder `enabled` flag plus the per-user
|
||||
// `notificationPrefs.measurementReminder.clientManaged` suppression.
|
||||
// NOT time-sensitive — a Vorsorge nudge is not a Focus-bypass case.
|
||||
"MEASUREMENT_REMINDER",
|
||||
] as const;
|
||||
export type EventType = (typeof EVENT_TYPES)[number];
|
||||
|
||||
@@ -102,6 +112,11 @@ export const EVENT_DEFAULT_ENABLED: Record<EventType, boolean> = {
|
||||
// push per medication per crossing. An explicit per-channel
|
||||
// `NotificationPreference` row still wins.
|
||||
MEDICATION_LOW_STOCK: true,
|
||||
// v1.17.1 — ON at the channel layer; the real gates are the per-reminder
|
||||
// `enabled` flag and the per-user `measurementReminder.clientManaged`
|
||||
// suppression the cron reads. An explicit per-channel
|
||||
// `NotificationPreference` row still wins.
|
||||
MEASUREMENT_REMINDER: true,
|
||||
};
|
||||
|
||||
export const CHANNEL_TYPE_LABELS: Record<ChannelType, string> = {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DEFAULT_NOTIFICATION_PREFS,
|
||||
DEFAULT_REORDER_LEAD_DAYS,
|
||||
isCycleReminderClientManaged,
|
||||
isMeasurementReminderClientManaged,
|
||||
isMedicationReminderClientManaged,
|
||||
notificationPrefsSchema,
|
||||
parseNotificationPrefs,
|
||||
@@ -109,6 +110,9 @@ describe("parseNotificationPrefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +133,9 @@ describe("parseNotificationPrefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,6 +158,9 @@ describe("parseNotificationPrefs", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -189,6 +199,9 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,6 +228,9 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,6 +254,9 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,6 +280,9 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -285,6 +307,9 @@ describe("resolveNotificationPrefs (deep-merge)", () => {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -356,6 +381,38 @@ describe("isCycleReminderClientManaged — cron-skip gate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isMeasurementReminderClientManaged — cron-skip gate", () => {
|
||||
it("returns false for a null / undefined row (server-managed default)", () => {
|
||||
expect(isMeasurementReminderClientManaged(null)).toBe(false);
|
||||
expect(isMeasurementReminderClientManaged(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when the user has not opted in", () => {
|
||||
expect(
|
||||
isMeasurementReminderClientManaged({
|
||||
measurementReminder: { clientManaged: false },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when the iOS app owns local Vorsorge reminders", () => {
|
||||
expect(
|
||||
isMeasurementReminderClientManaged({
|
||||
measurementReminder: { clientManaged: true },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is independent of the cycle / medication client-managed flags", () => {
|
||||
expect(
|
||||
isMeasurementReminderClientManaged({
|
||||
cycle: { clientManaged: true },
|
||||
medication: { deliveryDefault: "client" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMoodReminderHour — cron hour gate", () => {
|
||||
it("returns the default hour for a null row", () => {
|
||||
expect(resolveMoodReminderHour(null)).toBe(DEFAULT_MOOD_REMINDER_HOUR);
|
||||
|
||||
@@ -89,6 +89,19 @@ const cyclePrefsSchema = z
|
||||
})
|
||||
.partial();
|
||||
|
||||
/**
|
||||
* v1.17.1 — measurement / Vorsorge reminder category schema.
|
||||
* `clientManaged` mirrors the medication + cycle gate: when the iOS app
|
||||
* owns local Vorsorge reminders it flips this to `true` and the
|
||||
* server-side measurement-reminder cron suppresses its APNs send for that
|
||||
* user. Sub-object form keeps the layout open for future Vorsorge knobs.
|
||||
*/
|
||||
const measurementReminderPrefsSchema = z
|
||||
.object({
|
||||
clientManaged: z.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
/**
|
||||
* v1.15.20 — coach category schema. `nudgesEnabled` gates the proactive
|
||||
* Coach nudge cron (05:15 Europe/Berlin): default ON (the nudge is an
|
||||
@@ -134,6 +147,7 @@ export const notificationPrefsSchema = z
|
||||
mood: moodPrefsSchema,
|
||||
cycle: cyclePrefsSchema,
|
||||
coach: coachPrefsSchema,
|
||||
measurementReminder: measurementReminderPrefsSchema,
|
||||
})
|
||||
.partial();
|
||||
|
||||
@@ -187,6 +201,14 @@ export interface NotificationPrefs {
|
||||
/** v1.16.5 — cap interval: "weekly" (7 d) or "biweekly" (14 d). */
|
||||
nudgeFrequency: "weekly" | "biweekly";
|
||||
};
|
||||
measurementReminder: {
|
||||
/**
|
||||
* v1.17.1 — when true the iOS app owns local Vorsorge reminders and
|
||||
* the server-side measurement-reminder cron suppresses its APNs send
|
||||
* (the medication / cycle `clientManaged` precedent).
|
||||
*/
|
||||
clientManaged: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,6 +257,9 @@ export const DEFAULT_NOTIFICATION_PREFS: NotificationPrefs = {
|
||||
nudgeRoutine: true,
|
||||
nudgeFrequency: "weekly",
|
||||
},
|
||||
measurementReminder: {
|
||||
clientManaged: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -279,6 +304,10 @@ export function resolveNotificationPrefs(
|
||||
...base.coach,
|
||||
...(incoming.coach ?? {}),
|
||||
},
|
||||
measurementReminder: {
|
||||
...base.measurementReminder,
|
||||
...(incoming.measurementReminder ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,6 +336,7 @@ function applyDeliveryDefaultMapping(
|
||||
mood: { ...prefs.mood },
|
||||
cycle: { ...prefs.cycle },
|
||||
coach: { ...prefs.coach },
|
||||
measurementReminder: { ...prefs.measurementReminder },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -335,6 +365,16 @@ export function isCycleReminderClientManaged(raw: unknown): boolean {
|
||||
return parseNotificationPrefs(raw).cycle.clientManaged === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.17.1 — cron-side helper. Returns `true` when the user has opted in
|
||||
* to client-managed Vorsorge (measurement) reminders so the server-side
|
||||
* push should be suppressed. Tolerates a null / missing prefs row.
|
||||
* Mirrors `isMedicationReminderClientManaged` / `isCycleReminderClientManaged`.
|
||||
*/
|
||||
export function isMeasurementReminderClientManaged(raw: unknown): boolean {
|
||||
return parseNotificationPrefs(raw).measurementReminder.clientManaged === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.7.0 — resolve the effective delivery channel for a device. The
|
||||
* per-device override wins; otherwise the user-level roaming default;
|
||||
@@ -426,6 +466,9 @@ function cloneDefaults(): NotificationPrefs {
|
||||
mood: { ...DEFAULT_NOTIFICATION_PREFS.mood },
|
||||
cycle: { ...DEFAULT_NOTIFICATION_PREFS.cycle },
|
||||
coach: { ...DEFAULT_NOTIFICATION_PREFS.coach },
|
||||
measurementReminder: {
|
||||
...DEFAULT_NOTIFICATION_PREFS.measurementReminder,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -447,5 +490,9 @@ function mergeOverDefaults(input: NotificationPrefsInput): NotificationPrefs {
|
||||
...DEFAULT_NOTIFICATION_PREFS.coach,
|
||||
...(input.coach ?? {}),
|
||||
},
|
||||
measurementReminder: {
|
||||
...DEFAULT_NOTIFICATION_PREFS.measurementReminder,
|
||||
...(input.measurementReminder ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user