From 8a09adc1c0092f740b1420a36b111b9ffe1c90d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 24 Sep 2026 12:43:41 +0200 Subject: [PATCH] fix(cycle): answer a delete of an already deleted cycle without moving anything The cycle delete now runs the shared removal, which finds live starts only. A second delete of the same cycle read nothing to remove; the route now says so explicitly and answers 204 before the transaction, and the tombstone the first delete left stays as it was. --- src/app/api/cycle/cycles/[id]/route.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/api/cycle/cycles/[id]/route.ts b/src/app/api/cycle/cycles/[id]/route.ts index 1a163c633..088c6c7a0 100644 --- a/src/app/api/cycle/cycles/[id]/route.ts +++ b/src/app/api/cycle/cycles/[id]/route.ts @@ -37,17 +37,21 @@ export const DELETE = apiHandler( const existing = await prisma.menstrualCycle.findUnique({ where: { id }, - select: { id: true, userId: true, startDate: true }, + select: { id: true, userId: true, startDate: true, deletedAt: true }, }); if (!existing || existing.userId !== user.id) { return apiError("Cycle not found", 404); } + // Already a tombstone: the delete is done, and nothing is left to move. + if (existing.deletedAt !== null) { + return new Response(null, { status: 204 }); + } // Tombstone + re-anchor as one unit: a neighbour re-derived against a // half-applied delete would read the row being removed as still live. // The shared removal also gives back a start this one folded in and - // hands the removed cycle's days back. `null` is a cycle already - // tombstoned: nothing left to move. + // hands the removed cycle's days back. `null` is a concurrent delete that + // tombstoned the row first: nothing left to move. const removed = await prisma.$transaction((db) => removeCycleStartedOn(db, user.id, existing.startDate), );