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

@@ -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,
},
},
},
};