fix(nutrients): recompute supplement rows when the module returns or the zone moves

This commit is contained in:
Marc-André Bombeck committed 2026-10-16 05:49:58 +02:00
1 parent 37645fc4bb
commit cc9d523bc8
7 files changed
+96

No files matched your search

+7
View File
@@ -44,6 +44,7 @@ import {
} from "@/lib/optimistic-lock";
import { resolveModuleMap, normalisePrefs } from "@/lib/modules/gate";
import { modulePrefsPatchSchema } from "@/lib/validations/modules";
import { recomputeSupplementsIfNutrientsTurnedOn } from "@/lib/medications/ingredients/recompute-job";
export const dynamic = "force-dynamic";
@@ -140,6 +141,12 @@ export const PATCH = apiHandler(async (req: Request) => {
});
if ("conflict" in guarded) return guarded.conflict;
await recomputeSupplementsIfNutrientsTurnedOn(
user.id,
existing?.modulePreferencesJson,
merged,
);
// A module toggle can add or remove a Health Score pillar, so the
// cached composite was computed from a composition that no longer
// holds. Without this the score stays stale for up to an hour after a
@@ -31,6 +31,12 @@ vi.mock("@/lib/rollups/medication-compliance-rollups", () => ({
enqueueUserMedicationComplianceBackfill: refoldMock,
}));
// The supplement nutrient rows are keyed by the same local day.
const nutrientMock = vi.hoisted(() => vi.fn(async () => ({ queued: true })));
vi.mock("@/lib/medications/ingredients/recompute-job", () => ({
enqueueSupplementNutrientRecompute: nutrientMock,
}));
vi.mock("next/headers", () => ({
headers: vi.fn(async () => ({ get: () => null })),
cookies: vi.fn(async () => ({
@@ -118,6 +124,7 @@ describe("PUT /api/auth/me/timezone", () => {
const res = await PUT(mkReq({ timezone: "America/New_York" }));
expect(res.status).toBe(200);
expect(refoldMock).toHaveBeenCalledWith("user-1");
expect(nutrientMock).toHaveBeenCalledWith("user-1");
});
it("does not re-fold when the zone is unchanged", async () => {
@@ -130,6 +137,7 @@ describe("PUT /api/auth/me/timezone", () => {
const res = await PUT(mkReq({ timezone: "Europe/Berlin" }));
expect(res.status).toBe(200);
expect(refoldMock).not.toHaveBeenCalled();
expect(nutrientMock).not.toHaveBeenCalled();
});
it("trims whitespace before validating", async () => {
+3
View File
@@ -29,6 +29,7 @@ import {
import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { invalidateUserTimezone, isValidTimezone } from "@/lib/tz/resolver";
import { enqueueSupplementNutrientRecompute } from "@/lib/medications/ingredients/recompute-job";
import { enqueueUserMedicationComplianceBackfill } from "@/lib/rollups/medication-compliance-rollups";
export const PUT = apiHandler(async (request: NextRequest) => {
@@ -69,6 +70,8 @@ export const PUT = apiHandler(async (request: NextRequest) => {
// job; best-effort, the singleton key collapses rapid re-picks.
if ((before?.timezone ?? null) !== tz) {
const refold = await enqueueUserMedicationComplianceBackfill(user.id);
// The supplement nutrient rows are keyed by the same local day.
await enqueueSupplementNutrientRecompute(user.id);
annotate({
meta: {
timezone_compliance_refold_enqueued: refold.enqueued,
@@ -1,5 +1,6 @@
import type { NextRequest } from "next/server";
import { enqueueSupplementNutrientRecompute } from "@/lib/medications/ingredients/recompute-job";
import {
apiHandler,
requireGuardianAuth,
@@ -239,6 +240,8 @@ export const PATCH = apiHandler(
}
if (patch.timezone !== undefined) {
invalidateUserMedications(access.recordId, { evict: true });
// The supplement nutrient rows are keyed by the local day.
await enqueueSupplementNutrientRecompute(access.recordId);
}
settings = {
...record,