From c47b7dade3b346ca718172e23998881c9f12cdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 11:43:29 +0200 Subject: [PATCH] fix(comprehensive-aggregator): exclude soft-deleted in raw aggregate The route's central analytics aggregator runs eight `FROM measurements` clauses (window_stats CTE + main aggregate + DISTINCT ON latest + firstAt probe, mirrored across the rollup-fresh and cold-fallback branches) plus one `findMany` for the BP raw rows. None filtered on `deleted_at`, so the Coach/Insights snapshot would see tombstoned readings in every windowed avg, slope, anomaly count, and latest attribution. Add `AND m."deleted_at" IS NULL` to the eight raw SQL filters and `deletedAt: null` to `fetchBpRawRows`. The `measurement_rollups` findMany branch already rides the writer-side filter. --- src/lib/insights/comprehensive-aggregator.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib/insights/comprehensive-aggregator.ts b/src/lib/insights/comprehensive-aggregator.ts index b0123789f..0591ed0be 100644 --- a/src/lib/insights/comprehensive-aggregator.ts +++ b/src/lib/insights/comprehensive-aggregator.ts @@ -310,6 +310,7 @@ async function fetchBpRawRows( userId, type: { in: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"] }, measuredAt: { gte: ninetyDaysAgo }, + deletedAt: null, }, orderBy: { measuredAt: "asc" }, select: { type: true, measuredAt: true, value: true }, @@ -348,6 +349,7 @@ async function buildFromRollups( FROM measurements m WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL GROUP BY m."type" ) SELECT @@ -414,6 +416,7 @@ async function buildFromRollups( JOIN window_stats ws ON ws."type" = m."type" WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL GROUP BY m."type", ws.stddev_value `), timeSubquery(timings, "latests", () => prisma.$queryRaw` @@ -424,6 +427,7 @@ async function buildFromRollups( FROM measurements m WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL ORDER BY m."type", m."measured_at" DESC `), // v1.4.35 — DAY rollup buckets for the trailing 90 days, every @@ -450,6 +454,7 @@ async function buildFromRollups( FROM measurements m WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL `), ]); @@ -536,6 +541,7 @@ async function buildFromLiveAggregate( FROM measurements m WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL GROUP BY m."type" ) SELECT @@ -602,6 +608,7 @@ async function buildFromLiveAggregate( JOIN window_stats ws ON ws."type" = m."type" WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL GROUP BY m."type", ws.stddev_value `), timeSubquery(timings, "latests", () => prisma.$queryRaw` @@ -612,6 +619,7 @@ async function buildFromLiveAggregate( FROM measurements m WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL ORDER BY m."type", m."measured_at" DESC `), // Buckets may exist for some types even when the COUNT probe came @@ -672,6 +680,7 @@ async function buildFromLiveAggregate( FROM measurements m WHERE m."user_id" = ${userId} AND m."measured_at" >= ${ninetyDaysAgo} + AND m."deleted_at" IS NULL `, ); firstMeasurementAt = firstRows[0]?.first_at