Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 155 additions & 42 deletions lib/report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,7 +52,7 @@ class Report {
'food',
'pumpSettings',
'upload',
'dosingDecision'
'dosingDecision',
];

#dosingDecisionReasons = [
Expand Down Expand Up @@ -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(',');

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -865,58 +950,86 @@ 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,
},
},
).catch((error) => {
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]);
}
}
}
}
Expand All @@ -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');
Expand Down
86 changes: 86 additions & 0 deletions test/report.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});