feat(reminders): add server-side completion route for Vorsorge reminders

Add POST /api/measurement-reminders/{id}/complete so a client can mark a
measurement reminder done on the server, rather than only dismissing the
notification locally. The route is the explicit user-action equivalent of
the cron auto-resolve: it routes through the one shared satisfaction
primitive, stamps lastSatisfiedAt, re-anchors the server-authoritative
nextDueAt, and fires no notification of its own.

Idempotent via the primitive's forward-only guard — completing an
already-completed or auto-satisfied reminder returns 200 with
completed=false so a repeat tap need not be special-cased. Owner-scoped;
a cross-user or tombstoned id 404s and never satisfies. No schema change:
lastSatisfiedAt already records the completion instant.

Extend the OpenAPI contract with the new path and the completion result
schema, and cover the route with unit tests (marks done, idempotent
no-op, owner-scoped 404, no double-notify).
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 21:39:24 +02:00
1 parent ae3091df9e
commit 70f92558e6
5 files changed
+321

No files matched your search

@@ -184,3 +184,26 @@ export const measurementReminderDto = z
description:
"A Vorsorge reminder. nextDueAt is server-computed (server-authoritative). A free-text reminder carries measurementType=null and resolves only on a manual satisfy. origin distinguishes a user-created (VORSORGE) reminder from one minted by a Coach cadence suggestion (COACH); endsOn bounds a finite course window (null = open-ended).",
});
/**
* v1.18.6 — explicit-completion result. The `complete` route is the
* user-action equivalent of the cron auto-satisfy: it stamps
* `lastSatisfiedAt` and re-anchors `nextDueAt` through the SAME shared
* primitive, so it fires no notification of its own and is idempotent.
*
* `completed` reports whether THIS call advanced the reminder (true) or was
* a forward-only no-op because a prior satisfy / matching reading already
* fulfilled the current cycle (false). Either way the response is a 200 so
* a client that completes an already-completed reminder need not special-case
* the second tap. `reminder` carries the canonical post-completion DTO.
*/
export const measurementReminderCompletionDto = z
.object({
completed: z.boolean(),
reminder: measurementReminderDto,
})
.meta({
id: "MeasurementReminderCompletion",
description:
"Result of an explicit reminder completion. completed=true when this call advanced lastSatisfiedAt; completed=false when an earlier satisfy or a matching reading had already fulfilled the current cycle (idempotent no-op). reminder is the canonical post-completion DTO.",
});