merge: measurement-reminder completion route

Add an idempotent, owner-scoped completion endpoint for measurement
reminders that records satisfaction without firing a notification.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 22:05:22 +02:00
commit 39505a5d35
5 files changed
+321

No files matched your search

@@ -12,6 +12,7 @@ import {
createMeasurementReminderSchema,
updateMeasurementReminderSchema,
measurementReminderDto,
measurementReminderCompletionDto,
} from "@/lib/validations/measurement-reminders";
import { dataEnvelope, errorEnvelope, stdResponses } from "./shared";
@@ -169,4 +170,29 @@ export const measurementReminderPaths: NonNullable<
},
},
},
"/api/measurement-reminders/{id}/complete": {
post: {
tags: ["MeasurementReminders"],
summary: "Complete a Vorsorge reminder (v1.18.6)",
description:
"Explicit user-action completion (iOS #23): the app marks a reminder done server-side instead of only dismissing it locally. Routes through the same satisfaction primitive as the cron auto-resolve — stamps lastSatisfiedAt = now, re-anchors nextDueAt, fires no notification. Idempotent: completing an already-completed / auto-satisfied reminder returns 200 with completed=false. Owner-scoped.",
requestParams: { path: z.object({ id: z.string() }) },
responses: {
"200": {
description:
"Completion applied (completed=true) or already-satisfied no-op (completed=false); reminder carries the canonical post-completion DTO.",
content: {
"application/json": {
schema: dataEnvelope(
measurementReminderCompletionDto,
"MeasurementReminderCompleteEnvelope",
),
},
},
},
...reminderNotFound,
...stdResponses,
},
},
},
};
@@ -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.",
});