fix(reminders): decide what stays due by the cycle, not the measurement type

The first cut kept only free-text check-ups due after their reminder
and let every reminder with a measurement type roll on. A fortnightly
PHQ-9 or GAD-7 reminder is a measurement reminder, and each one still
jumped two weeks forward the morning it was sent without the
questionnaire being filled.

The rule is now the cycle length. A reminder whose next slot is more
than seven days away, or that has no cadence, stays due after its
reminder until it is satisfied (a matching reading, a lab result or
"done"), skipped or snoozed, and is sent again at most once a week. A
reminder on a weekly or shorter cycle rolls on as before. Appointments
stay one-shot. A reading that lands on an open measurement reminder
still sets lastSatisfiedAt and schedules the next cycle from itself.

The 0355 repair drops its "no measurement type" condition for the same
cycle rule, so the fortnightly questionnaires in the reported shape
are put back on the day they were reminded.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 10:33:13 +02:00
1 parent 4d4a527c0d
commit fb960fb72e
7 files changed
+296 -82

No files matched your search

@@ -1,22 +1,23 @@
-- An open check-up stays due after its reminder.
--
-- Until now the reminder tick rolled every delivered reminder on to its next
-- slot. For a check-up (a reminder with no measurement type, made by the
-- person or suggested by the Coach) that filed an open task under next month
-- or next year the moment it was reminded, without it having been done: the
-- start page and the check-up card then showed it as scheduled far ahead,
-- and a one-shot check-up disappeared altogether.
-- slot. For a reminder on a cycle longer than a week (a fortnightly
-- questionnaire, a yearly check-up) that filed an open task under a slot
-- weeks or a year away the moment it was reminded, without it having been
-- done: the start page and the check-up card then showed it as scheduled far
-- ahead, and a one-shot check-up disappeared altogether.
--
-- From this release a check-up keeps its due date until it is marked done,
-- skipped or snoozed, and is reminded again at most once a week while it
-- stays open. Three steps.
-- From this release such a reminder keeps its due date until it is satisfied
-- (a matching reading or lab result, or marked done), skipped or snoozed, and
-- is reminded again at most once a week while it stays open. A reminder on a
-- weekly or shorter cycle still rolls on. Three steps.
-- 1. The cursor that keeps the repeat to once a week: when a reminder for
-- this row last reached a channel. Nullable, no default; a row that was
-- never reminded simply has none.
ALTER TABLE "measurement_reminders" ADD COLUMN "last_notified_at" TIMESTAMP(3);
-- 2. Put back the due date of check-ups the old behaviour rolled on.
-- 2. Put back the due date of reminders the old behaviour rolled on.
--
-- The evidence is the reminder claim ledger: every reminder send first
-- claims `measurement:<reminder id>:<local date>` in `notification_events`
@@ -24,8 +25,11 @@ ALTER TABLE "measurement_reminders" ADD COLUMN "last_notified_at" TIMESTAMP(3);
-- channel delivered, but the roll-on only happened after a delivery, so a
-- row counts as rolled on by a send only when ALL of these hold:
--
-- - it is a live, enabled check-up: not deleted, not disabled, origin
-- VORSORGE or COACH (never an appointment), no measurement type;
-- - it is live and enabled, origin VORSORGE or COACH (never an
-- appointment), and on a cycle the new behaviour holds: an interval
-- longer than seven days, a recurrence rule, or no cadence at all.
-- Measurement reminders are included; whether it names a measurement
-- type does not matter;
-- - it was not done or skipped after that send, and it carries no
-- snooze;
-- - the send was its most recent claim, and the row's last write
@@ -36,16 +40,19 @@ ALTER TABLE "measurement_reminders" ADD COLUMN "last_notified_at" TIMESTAMP(3);
-- alone;
-- - the due date it carries now is what the roll-on would have written
-- and what the new behaviour no longer writes: more than a week past
-- the send (a weekly or shorter check-up still rolls on, so it is not
-- repaired), or no due date at all for a check-up with no cadence.
-- the send (a weekly or shorter cycle, including a rule that recurs
-- within the week, still rolls on, so it is not repaired), or no due
-- date at all for a reminder with no cadence.
--
-- The due date goes back to the instant of that send, which is the day
-- the check-up was reminded and at its notify hour. `last_notified_at`
-- the reminder went out and at its notify hour. `last_notified_at`
-- takes the same instant, so the repeat waits a week from that send
-- instead of firing again at the next notify hour. A claim older than the
-- ledger's 90-day retention is gone, and a row rolled on by such a send
-- stays as it is: without the claim there is no evidence of which slot
-- was reminded. Nothing is deleted.
-- was reminded. A reading or lab result that satisfied the reminder
-- after the send moved `last_satisfied_at` past it, so such a row is
-- excluded above. Nothing is deleted.
WITH latest_claim AS (
SELECT DISTINCT ON (r."id")
r."id" AS "reminder_id",
@@ -65,7 +72,11 @@ WHERE r."id" = lc."reminder_id"
AND r."deleted_at" IS NULL
AND r."enabled" = TRUE
AND r."origin" IN ('VORSORGE', 'COACH')
AND r."measurement_type" IS NULL
AND (
r."interval_days" > 7
OR r."rrule" IS NOT NULL
OR (r."interval_days" IS NULL AND r."rrule" IS NULL)
)
AND r."snoozed_until" IS NULL
AND r."last_notified_at" IS NULL
AND (r."last_satisfied_at" IS NULL OR r."last_satisfied_at" < lc."sent_at")
+5 -4
View File
@@ -5483,10 +5483,11 @@ model MeasurementReminder {
skipCount Int @default(0) @map("skip_count")
/// v1.39.2 — when a reminder for this row last reached at least one channel.
/// Written by the reminder tick after a delivered dispatch, read by the same
/// tick. A check-up (no `measurementType`, not an appointment) no longer
/// rolls `nextDueAt` forward when it is reminded: it stays due until it is
/// done, skipped or snoozed, and this cursor is what keeps the repeat to at
/// most once a week. A nudge counts as "already sent for this slot" only
/// tick. A reminder on a cycle longer than a week, or a one-shot (never an
/// appointment), no longer rolls `nextDueAt` forward when it is reminded:
/// it stays due until it is satisfied (a reading, a lab result or "done"),
/// skipped or snoozed, and this cursor keeps the repeat to at most once a
/// week. A nudge counts as "already sent for this slot" only
/// while `lastNotifiedAt >= nextDueAt`, so a snooze, a skip or a satisfy
/// (each of which moves `nextDueAt`) opens a fresh slot that fires on its
/// own day. Operational state, deliberately not in the backup: a restored