mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
perf(cycle): scope the reminder cron to the in-window cohort and harden the period write
Push the cycle-reminder cron's gating into the query instead of loading every CyclePrediction row each tick: filter on the user's cycleTrackingEnabled / predictionEnabled relation and restrict to rows whose nextPeriodStart falls inside a tz-generous [today-grace, today+lead] string window. The per-tick cost now scales with the in-window cohort, not the whole table; the exact per-user-timezone window still runs in JS. Make the period-boundary shortcut transactional and idempotent: wrap the close-prior + open-new (or stamp-current) cycle mutations in a single $transaction so two concurrent taps cannot double-close a prior cycle or compute a length against a stale read, and add withIdempotency so an exact key replay is a no-op. The boundary day-log now ranks the incoming flow against any existing same-day entry and never downgrades a richer flow. Decode sync cursors additively: a live beta v1 token keeps its measurement / mood / intake watermarks and simply fresh-scans the two domains added in v2 (cycleDays, cycles) rather than forcing a full re-init. Correct the stale "no live consumer" comment. Thread the user's timezone through the Coach cycle snapshot so "today", day-of-cycle and days-until-period derive from the user's zone — matching the calendar and insights routes — instead of wall-clock Berlin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
15bece9849
commit
824dcbb01a
9 files changed
+358
-49
No files matched your search
@@ -233,6 +233,103 @@ describe("cycle period shortcut", () => {
|
||||
expect(json.data.cycle.startDate).toBe("2026-03-10");
|
||||
expect(json.data.dayLog.flow).toBe("MEDIUM");
|
||||
});
|
||||
|
||||
it("closes the prior cycle atomically when a new start is inserted", async () => {
|
||||
await loginAs(FEMALE_USER_ID);
|
||||
const prisma = getPrismaClient();
|
||||
const { POST } = await import("@/app/api/cycle/period/route");
|
||||
|
||||
await POST(
|
||||
jsonRequest("/api/cycle/period", "POST", {
|
||||
action: "start",
|
||||
date: "2026-04-01",
|
||||
loggedAt: "2026-04-01T08:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
await POST(
|
||||
jsonRequest("/api/cycle/period", "POST", {
|
||||
action: "start",
|
||||
date: "2026-04-29",
|
||||
loggedAt: "2026-04-29T08:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
const prior = await prisma.menstrualCycle.findFirstOrThrow({
|
||||
where: { userId: FEMALE_USER_ID, startDate: "2026-04-01" },
|
||||
});
|
||||
// Close-prior + open-new ran as one unit: the prior cycle is stamped.
|
||||
expect(prior.endDate).toBe("2026-04-28");
|
||||
expect(prior.lengthDays).toBe(28);
|
||||
});
|
||||
|
||||
it("replays the same idempotency key without a second mutation", async () => {
|
||||
await loginAs(FEMALE_USER_ID);
|
||||
const prisma = getPrismaClient();
|
||||
const { POST } = await import("@/app/api/cycle/period/route");
|
||||
const key = `period-${Date.now()}`;
|
||||
|
||||
const first = await POST(
|
||||
jsonRequest(
|
||||
"/api/cycle/period",
|
||||
"POST",
|
||||
{
|
||||
action: "start",
|
||||
date: "2026-05-10",
|
||||
loggedAt: "2026-05-10T08:00:00.000Z",
|
||||
},
|
||||
{ idempotencyKey: key },
|
||||
),
|
||||
);
|
||||
expect(first.status).toBe(200);
|
||||
|
||||
const replay = await POST(
|
||||
jsonRequest(
|
||||
"/api/cycle/period",
|
||||
"POST",
|
||||
{
|
||||
action: "start",
|
||||
date: "2026-05-10",
|
||||
loggedAt: "2026-05-10T08:00:00.000Z",
|
||||
},
|
||||
{ idempotencyKey: key },
|
||||
),
|
||||
);
|
||||
expect(replay.status).toBe(200);
|
||||
|
||||
const cycles = await prisma.menstrualCycle.count({
|
||||
where: { userId: FEMALE_USER_ID, startDate: "2026-05-10", deletedAt: null },
|
||||
});
|
||||
expect(cycles).toBe(1);
|
||||
});
|
||||
|
||||
it("never downgrades a richer same-day flow on the boundary write", async () => {
|
||||
await loginAs(FEMALE_USER_ID);
|
||||
const prisma = getPrismaClient();
|
||||
const dayLogs = await import("@/app/api/cycle/day-logs/route");
|
||||
const period = await import("@/app/api/cycle/period/route");
|
||||
|
||||
// A manual HEAVY entry exists for the day…
|
||||
await dayLogs.POST(
|
||||
jsonRequest("/api/cycle/day-logs", "POST", {
|
||||
date: "2026-06-01",
|
||||
flow: "HEAVY",
|
||||
loggedAt: "2026-06-01T07:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
// …a one-tap "start" (boundary flow MEDIUM) must not downgrade it.
|
||||
await period.POST(
|
||||
jsonRequest("/api/cycle/period", "POST", {
|
||||
action: "start",
|
||||
date: "2026-06-01",
|
||||
loggedAt: "2026-06-01T08:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
const row = await prisma.cycleDayLog.findFirstOrThrow({
|
||||
where: { userId: FEMALE_USER_ID, date: "2026-06-01", deletedAt: null },
|
||||
});
|
||||
expect(row.flow).toBe("HEAVY");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cycle calendar", () => {
|
||||
|
||||
Reference in new issue
Block a user