diff --git a/lib/report.mjs b/lib/report.mjs index 37a169f9..9be9562b 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 = [ @@ -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(','); @@ -784,7 +791,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 +827,84 @@ 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) { + 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: endDateBound, + restricted_token: token, + }, + pumpSettingsHeaders: { + headers: sessionHeader, + }, + }; + } + + return { latestDiabetesDatum: null, pumpSettingsParams: null, pumpSettingsHeaders: null }; + } + async generate() { if (this.#reportDates) { this.userDataQueryParams = this.userDataQueryOptions({ @@ -865,49 +950,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 +997,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 +1048,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..51cc6d22 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -1584,3 +1584,89 @@ describe('getStatsByChartType', () => { }); }); }); + +describe('Report.getLatestInsulinAndPumpSettingsParams', () => { + 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'; + + const latestInsulinUploadId = 'upload-insulin'; + const latestInsulinTime = '2025-01-30T12:00:00.000Z'; + + const userData = [ + { + 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' }, + ]; + + const { pumpSettingsParams } = Report.getLatestInsulinAndPumpSettingsParams( + userData, + startDate, + endDate, + 'test-token', + { session: 'stuff' }, + ); + + expect(pumpSettingsParams).toEqual({ + type: 'pumpSettings', + uploadId: latestInsulinUploadId, + latest: 1, + 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', + }); + }); + + 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(); + }); +});