diff --git a/src/app/api/import/__tests__/route.test.ts b/src/app/api/import/__tests__/route.test.ts index 5e0647b8f..3450bcce8 100644 --- a/src/app/api/import/__tests__/route.test.ts +++ b/src/app/api/import/__tests__/route.test.ts @@ -777,6 +777,61 @@ describe("POST /api/import — write failure classification", () => { }); }); + it("resumes a partially committed legacy mood import without duplicates", async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-05-10T08:00:00.000Z")); + + const committedKeys = new Set(); + let writeAttempt = 0; + vi.mocked(prisma.moodEntry.create).mockImplementation(async ({ data }) => { + writeAttempt++; + if (writeAttempt === 2) { + throw new Error("transient write failure"); + } + + const key = `${data.date}:${data.moodLoggedAt.toISOString()}`; + if (committedKeys.has(key)) { + throw new Prisma.PrismaClientKnownRequestError( + "Unique constraint failed", + { + code: "P2002", + clientVersion: "test", + meta: { target: ["userId", "date", "moodLoggedAt"] }, + }, + ); + } + committedKeys.add(key); + return {} as never; + }); + + const payload = { + moodEntries: [ + { date: "2026-05-01", mood: "GUT", score: 4 }, + { date: "2026-05-02", mood: "OKAY", score: 3 }, + ], + }; + + const failedResponse = await POST(request(payload)); + expect(failedResponse.status).toBe(503); + expectRetryableFailure(await failedResponse.json(), { + measurements: 0, + moodEntries: 1, + skipped: 0, + }); + + vi.advanceTimersByTime(1000); + const retryResponse = await POST(request(payload)); + + expect(retryResponse.status).toBe(200); + expect(await retryResponse.json()).toEqual({ + data: { measurements: 0, moodEntries: 1, skipped: 1 }, + error: null, + }); + expect(committedKeys.size).toBe(2); + + vi.useRealTimers(); + }); + it("returns a retryable failure for a generic mood write error", async () => { vi.mocked(prisma.moodEntry.create).mockRejectedValueOnce( new Error("write failed"), diff --git a/src/app/api/import/route.ts b/src/app/api/import/route.ts index 6e633e756..e11675c34 100644 --- a/src/app/api/import/route.ts +++ b/src/app/api/import/route.ts @@ -240,9 +240,12 @@ export const POST = apiHandler(async (request: NextRequest) => { // Import mood entries if (!writeFailed && data.moodEntries?.length) { - for (const e of data.moodEntries) { + for (const [index, e] of data.moodEntries.entries()) { try { - const loggedAt = e.loggedAt ?? new Date(); + // Keep legacy rows retry-stable while preserving distinct rows per payload. + const loggedAt = + e.loggedAt ?? + new Date(Date.parse(`${e.date}T12:00:00.000Z`) + index); if (e.externalId) { // v1.12.1 — idempotent re-import keyed on the source-stable id. // A second import of the same export updates the row in place