From 1c96aba31c93e0a331227e937826e8941ee1e628 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Wed, 12 Nov 2025 16:19:45 -0500 Subject: [PATCH 1/6] BACK-3250 Aligns pump settings with in-range insulin data Improves pump settings selection for date-bounded reports by aligning to the latest in-range insulin upload Ensures corresponding upload records are fetched when missing to keep device context accurate Extracts alignment logic into a testable helper and adds unit coverage for boundary cases Relates to BACK-3250 --- lib/report.mjs | 152 ++++++++++++++++++++++++++++++++------------ test/report.test.js | 53 +++++++++++++++ 2 files changed, 164 insertions(+), 41 deletions(-) diff --git a/lib/report.mjs b/lib/report.mjs index 37a169f9..7a32a2ec 100644 --- a/lib/report.mjs +++ b/lib/report.mjs @@ -7,10 +7,10 @@ import moment from 'moment-timezone'; import * as vizPrintUtil from '@tidepool/viz/dist/print.js'; import * as PDFKit from 'pdfkit'; +import blobStream from 'blob-stream'; import { fetchUserData, getServerTime, mgdLUnits, mmolLUnits, } from './utils.mjs'; -import blobStream from 'blob-stream'; const { DataUtil } = vizDataUtil; const { createPrintPDFPackage, utils: PrintPDFUtils } = vizPrintUtil; @@ -52,7 +52,7 @@ class Report { 'food', 'pumpSettings', 'upload', - 'dosingDecision' + 'dosingDecision', ]; #dosingDecisionReasons = [ @@ -784,7 +784,7 @@ class Report { async graphRendererOrca(data) { this.resp = await axios.post(process.env.PLOTLY_ORCA, { figure: data, - ...{ format: 'svg' }, + format: 'svg', }); return this.resp.data; } @@ -820,6 +820,48 @@ class Report { return processedImages; } + /** + * Determine the latest in-range insulin datum and the preferred pumpSettings fetch params. + * Pure helper to support testing alignment logic. + */ + static getLatestInsulinAndPumpSettingsParams(userData, startDate, endDate, token, sessionHeader) { + const start = moment.utc(startDate); + const end = moment.utc(endDate); + + const insulinDiabetesDatums = reject(userData, (d) => { + const datumTime = moment.utc(d.time); + return !includes(['bolus', 'basal'], d.type) + || datumTime.isBefore(start) + || datumTime.isAfter(end); + }); + + const latestDiabetesDatum = insulinDiabetesDatums.length > 0 + ? insulinDiabetesDatums.reduce((latest, current) => { + const currentTime = moment.utc(current.time); + const latestTime = moment.utc(latest.time); + return currentTime.isAfter(latestTime) ? current : latest; + }) + : null; + + if (latestDiabetesDatum && latestDiabetesDatum.uploadId) { + return { + latestDiabetesDatum, + pumpSettingsParams: { + type: 'pumpSettings', + uploadId: latestDiabetesDatum.uploadId, + latest: 1, + endDate: moment.utc(latestDiabetesDatum.time).toISOString(), + restricted_token: token, + }, + pumpSettingsHeaders: { + headers: sessionHeader, + }, + }; + } + + return { latestDiabetesDatum: null, pumpSettingsParams: null, pumpSettingsHeaders: null }; + } + async generate() { if (this.#reportDates) { this.userDataQueryParams = this.userDataQueryOptions({ @@ -865,49 +907,46 @@ class Report { ); if (this.#reportDates) { - // fetch the latest pump settings record for date bound reports - const pumpSettingsFetch = await fetchUserData( - this.#userDetail.userId, - { - headers: this.#requestData.sessionHeader, - params: { - type: 'pumpSettings', - latest: 1, - restricted_token: this.#requestData.token, - }, - }, - ).catch((error) => { - this.#log.error(error); - }); - - if (pumpSettingsFetch?.length > 0) { - userData.push(pumpSettingsFetch[0]); - } + const { startDate, endDate } = this.#reportDates; + const start = moment.utc(startDate); + const end = moment.utc(endDate); + + const { pumpSettingsParams } = Report.getLatestInsulinAndPumpSettingsParams( + userData, + start, + end, + this.#requestData.token, + this.#requestData.sessionHeader, + ); - const latestPumpSettings = find(userData, { - type: 'pumpSettings', - }); + let pumpSettingsToAdd = null; - const latestPumpSettingsUploadId = get( - latestPumpSettings || {}, - 'uploadId', - ); + if (pumpSettingsParams) { + const pumpSettingsForUploadFetch = await fetchUserData( + this.#userDetail.userId, + { + headers: this.#requestData.sessionHeader, + params: pumpSettingsParams, + }, + ).catch((error) => { + this.#log.error(error); + }); - const latestPumpSettingsUpload = find(userData, { - type: 'upload', - uploadId: latestPumpSettingsUploadId, - }); + if (pumpSettingsForUploadFetch?.length > 0) { + [pumpSettingsToAdd] = pumpSettingsForUploadFetch; + userData.push(pumpSettingsToAdd); + } + } - if (latestPumpSettingsUploadId && !latestPumpSettingsUpload) { - // If we have pump settings, but we don't have the corresponing upload record used - // to get the device source, we need to fetch it - const pumpSettingsUploadFetch = await fetchUserData( + if (!pumpSettingsToAdd) { + const pumpSettingsFetch = await fetchUserData( this.#userDetail.userId, { headers: this.#requestData.sessionHeader, params: { - type: 'upload', - uploadId: latestPumpSettingsUploadId, + type: 'pumpSettings', + latest: 1, + endDate: end.toISOString(), restricted_token: this.#requestData.token, }, }, @@ -915,8 +954,39 @@ class Report { this.#log.error(error); }); - if (pumpSettingsUploadFetch?.length > 0) { - userData.push(pumpSettingsUploadFetch[0]); + if (pumpSettingsFetch?.length > 0) { + [pumpSettingsToAdd] = pumpSettingsFetch; + userData.push(pumpSettingsToAdd); + } + } + + const latestPumpSettings = pumpSettingsToAdd || find(userData, { type: 'pumpSettings' }); + const latestPumpSettingsUploadId = get(latestPumpSettings || {}, 'uploadId'); + + if (latestPumpSettingsUploadId) { + const latestPumpSettingsUpload = find(userData, { + type: 'upload', + uploadId: latestPumpSettingsUploadId, + }); + + if (!latestPumpSettingsUpload) { + const pumpSettingsUploadFetch = await fetchUserData( + this.#userDetail.userId, + { + headers: this.#requestData.sessionHeader, + params: { + type: 'upload', + uploadId: latestPumpSettingsUploadId, + restricted_token: this.#requestData.token, + }, + }, + ).catch((error) => { + this.#log.error(error); + }); + + if (pumpSettingsUploadFetch?.length > 0) { + userData.push(pumpSettingsUploadFetch[0]); + } } } } @@ -935,7 +1005,7 @@ class Report { const latestTimezone = this.getLatestTimezone(data); if (latestTimezone && latestTimezone.name) { this.#log.debug(latestTimezone.message || `using latest timezone name "${latestTimezone.name}" from user data`); - this.#timezoneName = latestTimezone.name + this.#timezoneName = latestTimezone.name; } this.#log.debug('getting report options'); diff --git a/test/report.test.js b/test/report.test.js index f0af0b83..55040e71 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -1584,3 +1584,56 @@ describe('getStatsByChartType', () => { }); }); }); + +describe('Report.getLatestInsulinAndPumpSettingsParams', () => { + it('returns pumpSettings params for latest in-range insulin uploadId bounded by insulin time', () => { + const startDate = '2025-01-01T00:00:00.000Z'; + const endDate = '2025-01-31T00:00:00.000Z'; + + const latestInsulinUploadId = 'upload-insulin'; + const latestInsulinTime = '2025-01-30T12:00:00.000Z'; + + const userData = [ + { type: 'upload', uploadId: latestInsulinUploadId }, + { type: 'basal', time: '2025-01-10T00:00:00.000Z', uploadId: 'older-upload' }, + { type: 'bolus', time: latestInsulinTime, uploadId: latestInsulinUploadId }, + { type: 'basal', time: '2025-02-01T00:00:00.000Z', uploadId: 'out-of-range' }, + ]; + + const { pumpSettingsParams } = Report.getLatestInsulinAndPumpSettingsParams( + userData, + startDate, + endDate, + 'test-token', + { session: 'stuff' }, + ); + + expect(pumpSettingsParams).toEqual({ + type: 'pumpSettings', + uploadId: latestInsulinUploadId, + latest: 1, + endDate: moment.utc(latestInsulinTime).toISOString(), + restricted_token: 'test-token', + }); + }); + + it('returns null params when no in-range insulin data', () => { + const startDate = '2025-01-01T00:00:00.000Z'; + const endDate = '2025-01-31T00:00:00.000Z'; + + const userData = [ + { type: 'upload', uploadId: 'u1' }, + { type: 'basal', time: '2024-12-31T23:00:00.000Z', uploadId: 'u1' }, + ]; + + const { pumpSettingsParams } = Report.getLatestInsulinAndPumpSettingsParams( + userData, + startDate, + endDate, + 'test-token', + { session: 'stuff' }, + ); + + expect(pumpSettingsParams).toBeNull(); + }); +}); From 5e636443f17a4afdd037b73d1099f2b2dc6200b3 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Wed, 12 Nov 2025 20:53:30 -0500 Subject: [PATCH 2/6] [BACK-3250] Aligns pump settings to latest in-range data Aligns pump settings lookup to the latest in-range pump data for continuous datasets and to upload time for non-continuous datasets to better match visualisation behavior. Passes through only supported metadata needed for downstream settings alignment and corrects a minor comment typo. Extends tests to cover new bounding rules and validate behavior across dataset types. --- lib/report.mjs | 47 +++++++++++++++++++++++++++++++++++++++++++-- test/report.test.js | 39 ++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/lib/report.mjs b/lib/report.mjs index 7a32a2ec..9be9562b 100644 --- a/lib/report.mjs +++ b/lib/report.mjs @@ -474,11 +474,18 @@ class Report { }); if (latestPumpSettingsUploadId && !latestPumpSettingsUpload) { - // If we have pump settings, but we don't have the corresponing upload record used + // If we have pump settings, but we don't have the corresponding upload record used // to get the device source, we need to fetch it options.getPumpSettingsUploadRecordById = latestPumpSettingsUploadId; } + // Pass through metadata needed for downstream settings alignment + // Note: latestPumpData is not a supported metaData key in DataUtil, + // so we limit to allowed metaData fields here. + options.metaData = options.metaData + ? `${options.metaData}, latestPumpUpload, latestDatumByType` + : 'latestPumpUpload, latestDatumByType'; + options.type = this.#reportDataTypes.join(','); options['dosingDecision.reason'] = this.#dosingDecisionReasons.join(','); @@ -844,13 +851,49 @@ class Report { : null; if (latestDiabetesDatum && latestDiabetesDatum.uploadId) { + const latestUpload = userData.find( + (d) => d.type === 'upload' && d.uploadId === latestDiabetesDatum.uploadId, + ); + + const isContinuous = latestUpload?.dataSetType === 'continuous'; + + // For continuous datasets, align pump settings to the latest in-range pump data + // for this upload. For non-continuous datasets, align to the upload record time + // (mirrors viz behavior), falling back to insulin time if upload time is missing. + let endDateBound; + + if (isContinuous) { + const pumpDataForUpload = userData.filter((d) => { + if (!['basal', 'bolus'].includes(d.type)) return false; + if (d.uploadId !== latestDiabetesDatum.uploadId) return false; + const t = moment.utc(d.time); + return !t.isBefore(start) && !t.isAfter(end); + }); + + if (pumpDataForUpload.length > 0) { + const latestPumpData = pumpDataForUpload.reduce((latest, current) => { + const currentTime = moment.utc(current.time); + const latestTime = moment.utc(latest.time); + return currentTime.isAfter(latestTime) ? current : latest; + }); + + endDateBound = moment.utc(latestPumpData.time).toISOString(); + } else { + endDateBound = moment.utc(latestDiabetesDatum.time).toISOString(); + } + } else if (latestUpload?.time) { + endDateBound = moment.utc(latestUpload.time).toISOString(); + } else { + endDateBound = moment.utc(latestDiabetesDatum.time).toISOString(); + } + return { latestDiabetesDatum, pumpSettingsParams: { type: 'pumpSettings', uploadId: latestDiabetesDatum.uploadId, latest: 1, - endDate: moment.utc(latestDiabetesDatum.time).toISOString(), + endDate: endDateBound, restricted_token: token, }, pumpSettingsHeaders: { diff --git a/test/report.test.js b/test/report.test.js index 55040e71..51cc6d22 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -1586,7 +1586,7 @@ describe('getStatsByChartType', () => { }); describe('Report.getLatestInsulinAndPumpSettingsParams', () => { - it('returns pumpSettings params for latest in-range insulin uploadId bounded by insulin time', () => { + it('returns pumpSettings params for latest in-range insulin uploadId bounded by upload time for non-continuous datasets', () => { const startDate = '2025-01-01T00:00:00.000Z'; const endDate = '2025-01-31T00:00:00.000Z'; @@ -1594,7 +1594,9 @@ describe('Report.getLatestInsulinAndPumpSettingsParams', () => { const latestInsulinTime = '2025-01-30T12:00:00.000Z'; const userData = [ - { type: 'upload', uploadId: latestInsulinUploadId }, + { + type: 'upload', uploadId: latestInsulinUploadId, dataSetType: 'normal', time: '2025-01-31T00:00:00.000Z', + }, { type: 'basal', time: '2025-01-10T00:00:00.000Z', uploadId: 'older-upload' }, { type: 'bolus', time: latestInsulinTime, uploadId: latestInsulinUploadId }, { type: 'basal', time: '2025-02-01T00:00:00.000Z', uploadId: 'out-of-range' }, @@ -1612,7 +1614,38 @@ describe('Report.getLatestInsulinAndPumpSettingsParams', () => { type: 'pumpSettings', uploadId: latestInsulinUploadId, latest: 1, - endDate: moment.utc(latestInsulinTime).toISOString(), + endDate: moment.utc('2025-01-31T00:00:00.000Z').toISOString(), + restricted_token: 'test-token', + }); + }); + + it('returns pumpSettings params bounded by latest in-range pump data time for continuous datasets', () => { + const startDate = '2025-01-01T00:00:00.000Z'; + const endDate = '2025-01-31T00:00:00.000Z'; + + const uploadId = 'upload-continuous'; + + const userData = [ + { type: 'upload', uploadId, dataSetType: 'continuous' }, + { type: 'basal', time: '2025-01-05T00:00:00.000Z', uploadId }, + { type: 'bolus', time: '2025-01-10T00:00:00.000Z', uploadId }, + { type: 'bolus', time: '2025-02-01T00:00:00.000Z', uploadId }, // out of range + ]; + + const { pumpSettingsParams } = Report.getLatestInsulinAndPumpSettingsParams( + userData, + startDate, + endDate, + 'test-token', + { session: 'stuff' }, + ); + + // Should be bounded by latest in-range pump data time (2025-01-10), not the out-of-range datum + expect(pumpSettingsParams).toEqual({ + type: 'pumpSettings', + uploadId, + latest: 1, + endDate: moment.utc('2025-01-10T00:00:00.000Z').toISOString(), restricted_token: 'test-token', }); }); From 7b10790b48638b656f5f2696308b9014f6cefc1f Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 8 Dec 2025 17:29:29 -0500 Subject: [PATCH 3/6] [WEB-3766] add deviceEvent to report query --- lib/report.mjs | 1 + test/report.test.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/report.mjs b/lib/report.mjs index 9be9562b..1ab45a16 100644 --- a/lib/report.mjs +++ b/lib/report.mjs @@ -53,6 +53,7 @@ class Report { 'pumpSettings', 'upload', 'dosingDecision', + 'deviceEvent', ]; #dosingDecisionReasons = [ diff --git a/test/report.test.js b/test/report.test.js index 51cc6d22..d26943a6 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -464,6 +464,10 @@ describe('report', () => { moment(uploadData[3].time).subtract(30, 'days').toISOString(), ); }); + + it('should include deviceEvent in the type list', () => { + expect(opts.type).toContain('deviceEvent'); + }); }); describe('when dates params used', () => { From 64050909d7fd08bac69973fa4875f91df9077ffe Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 8 Dec 2025 21:55:53 -0500 Subject: [PATCH 4/6] [WEB-3766] Refactor pump settings parameter handling for continuous data Restructures how pump settings parameters are constructed and passed to improve accuracy for continuous vs non-continuous datasets. Adds conditional logic to set endDate only for continuous data and includes debug logging for better traceability. --- lib/report.mjs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/report.mjs b/lib/report.mjs index 1ab45a16..aae7cf45 100644 --- a/lib/report.mjs +++ b/lib/report.mjs @@ -888,15 +888,21 @@ class Report { endDateBound = moment.utc(latestDiabetesDatum.time).toISOString(); } + const pumpSettingsParams = { + type: 'pumpSettings', + uploadId: latestDiabetesDatum.uploadId, + latest: 1, + restricted_token: token, + }; + + // Non-continuous datasets have pumpSettings records after all diabetes data + if (isContinuous) { + pumpSettingsParams.endDate = endDateBound; + } + return { latestDiabetesDatum, - pumpSettingsParams: { - type: 'pumpSettings', - uploadId: latestDiabetesDatum.uploadId, - latest: 1, - endDate: endDateBound, - restricted_token: token, - }, + pumpSettingsParams, pumpSettingsHeaders: { headers: sessionHeader, }, @@ -961,10 +967,13 @@ class Report { end, this.#requestData.token, this.#requestData.sessionHeader, + this.#log, ); let pumpSettingsToAdd = null; + this.#log.debug('pumpSettingsParams ', pumpSettingsParams); + if (pumpSettingsParams) { const pumpSettingsForUploadFetch = await fetchUserData( this.#userDetail.userId, From 3253a0fe0d33f0ff22613d599e658990d1466986 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Mon, 8 Dec 2025 22:11:29 -0500 Subject: [PATCH 5/6] [WEB-3766] remove endDate from non-continuous test --- test/report.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/report.test.js b/test/report.test.js index d26943a6..85c03c91 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -1618,7 +1618,6 @@ describe('Report.getLatestInsulinAndPumpSettingsParams', () => { type: 'pumpSettings', uploadId: latestInsulinUploadId, latest: 1, - endDate: moment.utc('2025-01-31T00:00:00.000Z').toISOString(), restricted_token: 'test-token', }); }); From a224e47654fbd2391bbd0590655f51f8b68e5c54 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Tue, 9 Dec 2025 10:35:52 -0500 Subject: [PATCH 6/6] Update lib/report.mjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/report.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/report.mjs b/lib/report.mjs index aae7cf45..dfd29f9d 100644 --- a/lib/report.mjs +++ b/lib/report.mjs @@ -967,7 +967,6 @@ class Report { end, this.#requestData.token, this.#requestData.sessionHeader, - this.#log, ); let pumpSettingsToAdd = null;