mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(nutrients): tell the account holder when a sync arrived but nothing landed
GET /api/nutrients now looks up the most recent nutrient.batch.ingest audit row when the window is genuinely empty, and surfaces it as lastAttempt only when that call landed nothing (inserted === 0 && updated === 0). A real success elsewhere is left alone, so an empty window for an unrelated reason never gets reframed as a failure. The lookup runs on the zero-rows path only, never on the happy path, and stays counts-and-reason-codes only - no user-typed content, no per-entry health values. The reason code never reaches the page as text: each of the four closed skip reasons maps to its own human sentence, and the two that have no useful action for the account holder (unit_mismatch, upsert_failed) say plainly that the entries couldn't be stored rather than inventing advice. day_invalid is the one case worth naming a real cause for - a clock or time zone mismatch on the sending side - without claiming it was a phone, since any token holder can call this route. This depends on AuditLog retention: cleanupOldAuditLogs prunes rows past 365 days by default, so a dormant account's last failed attempt can age out and the explanation silently reverts to the plain empty state. Documented at the lookup for whoever next touches that job.
This commit is contained in:
1 parent
3ec3a8c6aa
commit
12d3239de0
12 files changed
+374
-17
No files matched your search
@@ -301,4 +301,83 @@ describe("GET /api/nutrients (real Postgres)", () => {
|
||||
const res = await GET(new NextRequest("http://localhost/api/nutrients"));
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
// The account holder's only signal that a sync ever arrived: when the
|
||||
// window is genuinely empty AND the most recent nutrient-batch call
|
||||
// landed nothing, the read surfaces why instead of a bare empty state.
|
||||
it("surfaces the last failed attempt when the window is genuinely empty", async () => {
|
||||
const { POST } = await import("@/app/api/nutrients/batch/route");
|
||||
const { GET } = await import("@/app/api/nutrients/route");
|
||||
|
||||
await POST(
|
||||
postReq({
|
||||
entries: [
|
||||
{ day: recentDay(1), nutrient: "vitamin_d", unit: "mg", amount: 20 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const res = await GET(
|
||||
new NextRequest("http://localhost/api/nutrients?days=14"),
|
||||
);
|
||||
const json = (await res.json()) as {
|
||||
data: { nutrients: unknown[]; lastAttempt: { topReason: string } | null };
|
||||
};
|
||||
expect(json.data.nutrients).toHaveLength(0);
|
||||
expect(json.data.lastAttempt).toMatchObject({
|
||||
topReason: "unit_mismatch",
|
||||
});
|
||||
});
|
||||
|
||||
it("omits lastAttempt entirely once the window has data", async () => {
|
||||
const { POST } = await import("@/app/api/nutrients/batch/route");
|
||||
const { GET } = await import("@/app/api/nutrients/route");
|
||||
|
||||
await POST(
|
||||
postReq({
|
||||
entries: [
|
||||
{ day: recentDay(1), nutrient: "caffeine", unit: "mg", amount: 250 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const res = await GET(
|
||||
new NextRequest("http://localhost/api/nutrients?days=14"),
|
||||
);
|
||||
const json = (await res.json()) as {
|
||||
data: { nutrients: unknown[]; lastAttempt: unknown };
|
||||
};
|
||||
expect(json.data.nutrients).toHaveLength(1);
|
||||
expect(json.data.lastAttempt).toBeNull();
|
||||
});
|
||||
|
||||
// A last attempt that actually SUCCEEDED must never be reframed as a
|
||||
// failure just because the window is empty for an unrelated reason
|
||||
// (here: the stored row was removed after the fact). Showing a failure
|
||||
// notice over a real success is the exact false claim this feature
|
||||
// exists to prevent.
|
||||
it("stays silent when the last recorded attempt succeeded, even if the window is now empty", async () => {
|
||||
const { POST } = await import("@/app/api/nutrients/batch/route");
|
||||
const { GET } = await import("@/app/api/nutrients/route");
|
||||
|
||||
await POST(
|
||||
postReq({
|
||||
entries: [
|
||||
{ day: recentDay(1), nutrient: "caffeine", unit: "mg", amount: 250 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
await getPrismaClient().nutrientIntakeDay.deleteMany({
|
||||
where: { userId: OPTED_IN_USER },
|
||||
});
|
||||
|
||||
const res = await GET(
|
||||
new NextRequest("http://localhost/api/nutrients?days=14"),
|
||||
);
|
||||
const json = (await res.json()) as {
|
||||
data: { nutrients: unknown[]; lastAttempt: unknown };
|
||||
};
|
||||
expect(json.data.nutrients).toHaveLength(0);
|
||||
expect(json.data.lastAttempt).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user