diff --git a/README.md b/README.md index de9fafbe..ea833315 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,7 @@ In this section, we describe all user-related data to be stored. The security ru |invitationCode|string|-|The invitationCode to be used when logging in to the app for the first time.| |language|optional string|e.g. "en"|Following IETF BCP-47 / [FHIR ValueSet languages](https://hl7.org/fhir/R4B/valueset-languages.html).| |receivesAppointmentReminders|optional boolean|true, false|Decides whether to send out appointment reminders one day before each appointment.| +|receivesInactivityReminders|optional boolean|true, false|Decides whether to send updates about inactivity.| |receivesMedicationUpdates|optional boolean|true, false|Decides whether to send updates about current medication changes.| |receivesQuestionnaireReminders|optional boolean|true, false|Decides whether to send reminders about filling out their questionnaire every 14 days.| |receivesRecommendationUpdates|optional boolean|true, false|Decides whether to send updates about recommended medication changes.| diff --git a/functions/data/debug/users.json b/functions/data/debug/users.json index de2cf4a9..066a0b69 100644 --- a/functions/data/debug/users.json +++ b/functions/data/debug/users.json @@ -9,6 +9,7 @@ "user": { "type": "admin", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -22,6 +23,7 @@ "user": { "type": "admin", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -36,6 +38,7 @@ "type": "owner", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -50,6 +53,7 @@ "type": "owner", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -64,6 +68,7 @@ "type": "owner", "organization": "jhu", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -78,6 +83,7 @@ "type": "owner", "organization": "umich", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -92,6 +98,7 @@ "type": "owner", "organization": "uw", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -106,6 +113,7 @@ "type": "clinician", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -120,6 +128,7 @@ "type": "clinician", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -134,6 +143,7 @@ "type": "clinician", "organization": "jhu", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -148,6 +158,7 @@ "type": "clinician", "organization": "umich", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -162,6 +173,7 @@ "type": "clinician", "organization": "uw", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -176,6 +188,7 @@ "type": "patient", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -190,6 +203,7 @@ "type": "patient", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -204,6 +218,7 @@ "type": "patient", "organization": "stanford", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -218,6 +233,7 @@ "type": "patient", "organization": "jhu", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -232,6 +248,7 @@ "type": "patient", "organization": "jhu", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } }, @@ -246,6 +263,7 @@ "type": "patient", "organization": "jhu", "dateOfEnrollment": "1970-01-01T00:00:00.000Z", + "lastActiveDate": "1970-01-01T00:00:00.000Z", "invitationCode": "" } } diff --git a/functions/models/src/types/user.ts b/functions/models/src/types/user.ts index 29747a41..62568213 100644 --- a/functions/models/src/types/user.ts +++ b/functions/models/src/types/user.ts @@ -24,10 +24,12 @@ export const userConverter = new Lazy( .extend({ dateOfEnrollment: dateConverter.schema, invitationCode: z.string(), + lastActiveDate: dateConverter.schema, }) .transform((values) => new User(values)), encode: (object) => ({ ...userRegistrationConverter.value.encode(object), + lastActiveDate: dateConverter.encode(object.lastActiveDate), dateOfEnrollment: dateConverter.encode(object.dateOfEnrollment), invitationCode: object.invitationCode, }), @@ -39,6 +41,7 @@ export class User extends UserRegistration { readonly dateOfEnrollment: Date readonly invitationCode: string + readonly lastActiveDate: Date // Constructor @@ -48,6 +51,7 @@ export class User extends UserRegistration { dateOfBirth?: Date clinician?: string receivesAppointmentReminders: boolean + receivesInactivityReminders: boolean receivesMedicationUpdates: boolean receivesQuestionnaireReminders: boolean receivesRecommendationUpdates: boolean @@ -57,9 +61,11 @@ export class User extends UserRegistration { timeZone?: string dateOfEnrollment: Date invitationCode: string + lastActiveDate: Date }) { super(input) this.dateOfEnrollment = input.dateOfEnrollment this.invitationCode = input.invitationCode + this.lastActiveDate = input.lastActiveDate } } diff --git a/functions/models/src/types/userMessage.ts b/functions/models/src/types/userMessage.ts index 5f0529c3..d1a38575 100644 --- a/functions/models/src/types/userMessage.ts +++ b/functions/models/src/types/userMessage.ts @@ -26,6 +26,7 @@ export enum UserMessageType { vitals = 'Vitals', symptomQuestionnaire = 'SymptomQuestionnaire', preAppointment = 'PreAppointment', + inactive = 'Inactive', } export const userMessageConverter = new Lazy( @@ -67,6 +68,26 @@ export const userMessageConverter = new Lazy( export class UserMessage { // Static Functions + static createInactive(input: { + creationDate?: Date + reference?: string + isDismissible?: boolean + }): UserMessage { + return new UserMessage({ + creationDate: input.creationDate ?? new Date(), + title: new LocalizedText({ + en: 'Inactive', + }), + description: new LocalizedText({ + en: 'You have been inactive for 7 days. Please log in to continue your care.', + }), + action: undefined, + type: UserMessageType.inactive, + isDismissible: input.isDismissible ?? false, + reference: input.reference, + }) + } + static createMedicationChange(input: { creationDate?: Date reference: string @@ -91,6 +112,7 @@ export class UserMessage { static createMedicationUptitration( input: { creationDate?: Date + reference?: string } = {}, ): UserMessage { return new UserMessage({ @@ -101,6 +123,7 @@ export class UserMessage { description: new LocalizedText({ en: 'You may be eligible for med changes that may help your heart. Your care team will be sent this information. You can review med information on the Education Page.', }), + reference: input.reference, action: 'medications', type: UserMessageType.medicationUptitration, isDismissible: true, @@ -111,6 +134,7 @@ export class UserMessage { input: { creationDate?: Date reference?: string + isDismissible?: boolean } = {}, ): UserMessage { return new UserMessage({ @@ -123,7 +147,7 @@ export class UserMessage { }), action: 'healthSummary', type: UserMessageType.preAppointment, - isDismissible: false, + isDismissible: input.isDismissible ?? false, reference: input.reference, }) } @@ -170,6 +194,7 @@ export class UserMessage { static createWeightGain( input: { creationDate?: Date + reference?: string } = {}, ): UserMessage { return new UserMessage({ @@ -181,6 +206,7 @@ export class UserMessage { en: 'Your weight increased over 3 lbs. Your care team will be informed. Please follow any instructions about diuretic changes after weight increase on the Medication page.', }), action: 'medications', + reference: input.reference, type: UserMessageType.weightGain, isDismissible: true, }) diff --git a/functions/models/src/types/userRegistration.ts b/functions/models/src/types/userRegistration.ts index 836e6731..e57c265c 100644 --- a/functions/models/src/types/userRegistration.ts +++ b/functions/models/src/types/userRegistration.ts @@ -22,6 +22,7 @@ export const userRegistrationInputConverter = new Lazy( dateOfBirth: optionalish(dateConverter.schema), clinician: optionalish(z.string()), receivesAppointmentReminders: optionalishDefault(z.boolean(), true), + receivesInactivityReminders: optionalishDefault(z.boolean(), true), receivesMedicationUpdates: optionalishDefault(z.boolean(), true), receivesQuestionnaireReminders: optionalishDefault(z.boolean(), true), receivesRecommendationUpdates: optionalishDefault(z.boolean(), true), @@ -37,6 +38,7 @@ export const userRegistrationInputConverter = new Lazy( object.dateOfBirth ? dateConverter.encode(object.dateOfBirth) : null, clinician: object.clinician ?? null, receivesAppointmentReminders: object.receivesAppointmentReminders, + receivesInactivityReminders: object.receivesInactivityReminders, receivesMedicationUpdates: object.receivesMedicationUpdates, receivesQuestionnaireReminders: object.receivesQuestionnaireReminders, receivesRecommendationUpdates: object.receivesRecommendationUpdates, @@ -68,6 +70,7 @@ export class UserRegistration { readonly clinician?: string readonly receivesAppointmentReminders: boolean + readonly receivesInactivityReminders: boolean readonly receivesMedicationUpdates: boolean readonly receivesQuestionnaireReminders: boolean readonly receivesRecommendationUpdates: boolean @@ -85,6 +88,7 @@ export class UserRegistration { dateOfBirth?: Date clinician?: string receivesAppointmentReminders: boolean + receivesInactivityReminders: boolean receivesMedicationUpdates: boolean receivesQuestionnaireReminders: boolean receivesRecommendationUpdates: boolean @@ -98,6 +102,7 @@ export class UserRegistration { this.dateOfBirth = input.dateOfBirth this.clinician = input.clinician this.receivesAppointmentReminders = input.receivesAppointmentReminders + this.receivesInactivityReminders = input.receivesInactivityReminders this.receivesMedicationUpdates = input.receivesMedicationUpdates this.receivesQuestionnaireReminders = input.receivesQuestionnaireReminders this.receivesRecommendationUpdates = input.receivesRecommendationUpdates diff --git a/functions/src/functions/checkInvitationCode.test.ts b/functions/src/functions/checkInvitationCode.test.ts index de302772..2fdbbb8e 100644 --- a/functions/src/functions/checkInvitationCode.test.ts +++ b/functions/src/functions/checkInvitationCode.test.ts @@ -50,6 +50,7 @@ describeWithEmulators('function: checkInvitationCode', (env) => { type: UserType.patient, organization: 'stanford', receivesAppointmentReminders: true, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: true, receivesRecommendationUpdates: true, diff --git a/functions/src/functions/createInvitation.test.ts b/functions/src/functions/createInvitation.test.ts index afbc91d8..ad8ad526 100644 --- a/functions/src/functions/createInvitation.test.ts +++ b/functions/src/functions/createInvitation.test.ts @@ -27,6 +27,7 @@ describeWithEmulators('function: createInvitation', (env) => { type: UserType.clinician, organization: 'stanford', receivesAppointmentReminders: false, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: false, receivesRecommendationUpdates: true, @@ -57,6 +58,7 @@ describeWithEmulators('function: createInvitation', (env) => { type: UserType.patient, organization: 'stanford', receivesAppointmentReminders: false, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: false, receivesRecommendationUpdates: true, @@ -87,6 +89,7 @@ describeWithEmulators('function: createInvitation', (env) => { type: UserType.patient, organization: 'stanford', receivesAppointmentReminders: true, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: true, receivesRecommendationUpdates: true, diff --git a/functions/src/functions/defaultSeed.ts b/functions/src/functions/defaultSeed.ts index fcda0a20..3e8e3d46 100644 --- a/functions/src/functions/defaultSeed.ts +++ b/functions/src/functions/defaultSeed.ts @@ -20,6 +20,74 @@ import { Flags } from '../flags.js' import { UserRole } from '../services/credential/credential.js' import { getServiceFactory } from '../services/factory/getServiceFactory.js' import { type ServiceFactory } from '../services/factory/serviceFactory.js' +import { type DebugDataService } from '../services/seeding/debugData/debugDataService.js' +import { type TriggerService } from '../services/trigger/triggerService.js' + +async function _seedPatientCollections(input: { + debugData: DebugDataService + trigger: TriggerService + userId: string + components: UserDebugDataComponent[] + date: Date +}): Promise { + const promises: Array> = [] + if (input.components.includes(UserDebugDataComponent.appointments)) + promises.push( + input.debugData.seedUserAppointments(input.userId, input.date), + ) + if ( + input.components.includes(UserDebugDataComponent.bloodPressureObservations) + ) + promises.push( + input.debugData.seedUserBloodPressureObservations( + input.userId, + input.date, + ), + ) + if (input.components.includes(UserDebugDataComponent.bodyWeightObservations)) + promises.push( + input.debugData.seedUserBodyWeightObservations(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.creatinineObservations)) + promises.push( + input.debugData.seedUserCreatinineObservations(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.dryWeightObservations)) + promises.push( + input.debugData.seedUserDryWeightObservations(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.eGfrObservations)) + promises.push( + input.debugData.seedUserEgfrObservations(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.heartRateObservations)) + promises.push( + input.debugData.seedUserHeartRateObservations(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.potassiumObservations)) + promises.push( + input.debugData.seedUserPotassiumObservations(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.medicationRequests)) + promises.push(input.debugData.seedUserMedicationRequests(input.userId)) + if (input.components.includes(UserDebugDataComponent.messages)) + promises.push(input.debugData.seedUserMessages(input.userId, input.date)) + if (input.components.includes(UserDebugDataComponent.consent)) + promises.push(input.debugData.seedUserConsent(input.userId)) + if ( + input.components.includes(UserDebugDataComponent.medicationRecommendations) + ) + promises.push( + input.trigger.updateRecommendationsForUser(input.userId).then(), + ) + if (input.components.includes(UserDebugDataComponent.questionnaireResponses)) + promises.push( + input.debugData.seedUserQuestionnaireResponses(input.userId, input.date), + ) + if (input.components.includes(UserDebugDataComponent.symptomScores)) + promises.push(input.trigger.updateAllSymptomScores(input.userId)) + await Promise.all(promises) +} export async function _defaultSeed( factory: ServiceFactory, @@ -39,118 +107,15 @@ export async function _defaultSeed( for (const userId of userIds) { try { - const promises: Array> = [] const user = await userService.getUser(userId) if (user?.content.type !== UserType.patient) continue - if ( - data.onlyUserCollections.includes(UserDebugDataComponent.appointments) - ) - promises.push( - debugDataService.seedUserAppointments(userId, data.date), - ) - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.bloodPressureObservations, - ) - ) - promises.push( - debugDataService.seedUserBloodPressureObservations( - userId, - data.date, - ), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.bodyWeightObservations, - ) - ) - promises.push( - debugDataService.seedUserBodyWeightObservations(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.creatinineObservations, - ) - ) - promises.push( - debugDataService.seedUserCreatinineObservations(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.dryWeightObservations, - ) - ) - promises.push( - debugDataService.seedUserDryWeightObservations(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.eGfrObservations, - ) - ) - promises.push( - debugDataService.seedUserEgfrObservations(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.heartRateObservations, - ) - ) - promises.push( - debugDataService.seedUserHeartRateObservations(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.potassiumObservations, - ) - ) - promises.push( - debugDataService.seedUserPotassiumObservations(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.medicationRequests, - ) - ) - promises.push(debugDataService.seedUserMedicationRequests(userId)) - - if (data.onlyUserCollections.includes(UserDebugDataComponent.messages)) - promises.push(debugDataService.seedUserMessages(userId, data.date)) - if (data.onlyUserCollections.includes(UserDebugDataComponent.consent)) - promises.push(debugDataService.seedUserConsent(userId)) - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.medicationRecommendations, - ) - ) - promises.push( - triggerService.updateRecommendationsForUser(userId).then(), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.questionnaireResponses, - ) - ) - promises.push( - debugDataService.seedUserQuestionnaireResponses(userId, data.date), - ) - - if ( - data.onlyUserCollections.includes( - UserDebugDataComponent.symptomScores, - ) - ) - promises.push(triggerService.updateAllSymptomScores(userId)) - - await Promise.all(promises) + await _seedPatientCollections({ + debugData: debugDataService, + trigger: triggerService, + userId, + components: data.onlyUserCollections, + date: data.date, + }) } catch (error) { logger.error(`Failed to seed user ${userId}: ${String(error)}`) } @@ -159,87 +124,13 @@ export async function _defaultSeed( for (const userData of data.userData) { try { - const promises: Array> = [] - if (userData.only.includes(UserDebugDataComponent.appointments)) - promises.push( - debugDataService.seedUserAppointments(userData.userId, data.date), - ) - if (userData.only.includes(UserDebugDataComponent.medicationRequests)) - promises.push( - debugDataService.seedUserMedicationRequests(userData.userId), - ) - if (userData.only.includes(UserDebugDataComponent.messages)) - promises.push( - debugDataService.seedUserMessages(userData.userId, data.date), - ) - if ( - userData.only.includes(UserDebugDataComponent.bloodPressureObservations) - ) - promises.push( - debugDataService.seedUserBloodPressureObservations( - userData.userId, - data.date, - ), - ) - if (userData.only.includes(UserDebugDataComponent.bodyWeightObservations)) - promises.push( - debugDataService.seedUserBodyWeightObservations( - userData.userId, - data.date, - ), - ) - if (userData.only.includes(UserDebugDataComponent.creatinineObservations)) - promises.push( - debugDataService.seedUserCreatinineObservations( - userData.userId, - data.date, - ), - ) - if (userData.only.includes(UserDebugDataComponent.dryWeightObservations)) - promises.push( - debugDataService.seedUserDryWeightObservations( - userData.userId, - data.date, - ), - ) - if (userData.only.includes(UserDebugDataComponent.eGfrObservations)) - promises.push( - debugDataService.seedUserEgfrObservations(userData.userId, data.date), - ) - if (userData.only.includes(UserDebugDataComponent.heartRateObservations)) - promises.push( - debugDataService.seedUserHeartRateObservations( - userData.userId, - data.date, - ), - ) - if (userData.only.includes(UserDebugDataComponent.potassiumObservations)) - promises.push( - debugDataService.seedUserPotassiumObservations( - userData.userId, - data.date, - ), - ) - if (userData.only.includes(UserDebugDataComponent.consent)) - promises.push(debugDataService.seedUserConsent(userData.userId)) - if ( - userData.only.includes(UserDebugDataComponent.medicationRecommendations) - ) - promises.push( - triggerService.updateRecommendationsForUser(userData.userId).then(), - ) - if (userData.only.includes(UserDebugDataComponent.questionnaireResponses)) - promises.push( - debugDataService.seedUserQuestionnaireResponses( - userData.userId, - data.date, - ), - ) - - if (userData.only.includes(UserDebugDataComponent.symptomScores)) - promises.push(triggerService.updateAllSymptomScores(userData.userId)) - - await Promise.all(promises) + await _seedPatientCollections({ + debugData: debugDataService, + trigger: triggerService, + userId: userData.userId, + components: userData.only, + date: data.date, + }) } catch (error) { logger.error( `Failed to seed user data ${userData.userId}: ${String(error)}`, diff --git a/functions/src/functions/deleteInvitation.test.ts b/functions/src/functions/deleteInvitation.test.ts index a8ebe74e..fefacfd5 100644 --- a/functions/src/functions/deleteInvitation.test.ts +++ b/functions/src/functions/deleteInvitation.test.ts @@ -31,6 +31,7 @@ describeWithEmulators('function: deleteInvitation', (env) => { type: UserType.patient, organization: 'stanford', receivesAppointmentReminders: false, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: false, receivesRecommendationUpdates: true, diff --git a/functions/src/services/message/defaultMessageService.ts b/functions/src/services/message/defaultMessageService.ts index b1ee5a57..58ccf422 100644 --- a/functions/src/services/message/defaultMessageService.ts +++ b/functions/src/services/message/defaultMessageService.ts @@ -141,7 +141,13 @@ export class DefaultMessageService implements MessageService { .userMessages(userId) .where('completionDate', '==', null) .get() - ).docs.filter((doc) => doc.data().type === message.type) + ).docs.filter((doc) => { + const docData = doc.data() + return ( + docData.type === message.type && + docData.reference === message.reference + ) + }) logger.debug( `DatabaseMessageService.addMessage(user: ${userId}): Found ${existingMessages.length} existing messages`, @@ -294,22 +300,15 @@ export class DefaultMessageService implements MessageService { ) } else { logger.debug( - `DefaultMessageService.handleOldMessages(weightGain): Contains newish message at: ${oldMessage.ref.path}`, + `DefaultMessageService.handleOldMessages(${newMessage.type}): Contains newish message at: ${oldMessage.ref.path}`, ) containsNewishMessage = true } } logger.debug( - `DefaultMessageService.handleOldMessages(weightGain): Contains newish message? ${containsNewishMessage ? 'yes' : 'no'}`, + `DefaultMessageService.handleOldMessages(${newMessage.type}): Contains newish message? ${containsNewishMessage ? 'yes' : 'no'}`, ) return !containsNewishMessage - case UserMessageType.welcome: - case UserMessageType.medicationUptitration: - logger.debug( - `DefaultMessageService.handleOldMessages(${newMessage.type}): Only creating new message, if there are no old messages (count: ${oldMessages.length})`, - ) - // Keep only the most recent message - return oldMessages.length === 0 case UserMessageType.symptomQuestionnaire: case UserMessageType.vitals: // Mark old messages as completed and create new ones instead @@ -326,21 +325,15 @@ export class DefaultMessageService implements MessageService { ) } return true + case UserMessageType.welcome: + case UserMessageType.medicationUptitration: case UserMessageType.medicationChange: case UserMessageType.preAppointment: + case UserMessageType.inactive: logger.debug( `DefaultMessageService.handleOldMessages(${newMessage.type}): Only creating new message, if there are no old messages with the same reference (count: ${oldMessages.length})`, ) - // Keep old message, if it references the same entity - for (const oldMessage of oldMessages) { - if (oldMessage.data().reference === newMessage.reference) { - logger.debug( - `DefaultMessageService.handleOldMessages(${newMessage.type}): Found message with the same reference at ${oldMessage.ref.path}`, - ) - return false - } - } - return true + return oldMessages.length === 0 } } diff --git a/functions/src/services/seeding/debugData/debugDataService.ts b/functions/src/services/seeding/debugData/debugDataService.ts index d8324336..32f5e115 100644 --- a/functions/src/services/seeding/debugData/debugDataService.ts +++ b/functions/src/services/seeding/debugData/debugDataService.ts @@ -138,6 +138,9 @@ export class DebugDataService extends SeedingService { async seedUserMessages(userId: string, date: Date) { const values = [ + UserMessage.createInactive({ + creationDate: date, + }), UserMessage.createMedicationChange({ creationDate: date, medicationName: 'Losartan Potassium', diff --git a/functions/src/services/trigger/triggerService.test.ts b/functions/src/services/trigger/triggerService.test.ts index 2d5b853d..779c10c5 100644 --- a/functions/src/services/trigger/triggerService.test.ts +++ b/functions/src/services/trigger/triggerService.test.ts @@ -8,77 +8,119 @@ import { advanceDateByDays, + advanceDateByMinutes, FHIRAppointment, FHIRAppointmentStatus, + FHIRObservation, + LoincCode, + QuantityUnit, UserMessage, UserMessageType, UserType, } from '@stanfordbdhg/engagehf-models' import { expect } from 'chai' import { describeWithEmulators } from '../../tests/functions/testEnvironment.js' +import { UserObservationCollection } from '../database/collections.js' describeWithEmulators('TriggerService', (env) => { describe('every15Minutes', () => { it('should create a message for an upcoming appointment', async () => { - const userId = await env.createUser({ + const clinicianId = await env.createUser({ + type: UserType.clinician, + organization: 'stanford', + }) + + const patientId = await env.createUser({ type: UserType.patient, organization: 'stanford', + clinician: clinicianId, }) const appointment = FHIRAppointment.create({ - userId, + userId: patientId, status: FHIRAppointmentStatus.proposed, created: advanceDateByDays(new Date(), -3), start: advanceDateByDays(new Date(), 1.01), durationInMinutes: 60, }) - const appointmentRef = env.collections.userAppointments(userId).doc() + const appointmentRef = env.collections.userAppointments(patientId).doc() await appointmentRef.set(appointment) await env.factory.trigger().every15Minutes() - const messages = await env.collections.userMessages(userId).get() - expect(messages.docs).to.have.length(1) - const message = messages.docs.at(0)?.data() + const patientMessages = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages.docs).to.have.length(1) + const patientMessage = patientMessages.docs.at(0)?.data() + expect(patientMessage?.type).to.equal(UserMessageType.preAppointment) + expect(patientMessage?.reference).to.equal(appointmentRef.path) + expect(patientMessage?.completionDate).to.be.undefined - expect(message?.type).to.equal(UserMessageType.preAppointment) - expect(message?.reference).to.equal(appointmentRef.path) - expect(message?.completionDate).to.be.undefined + const clinicianMessages = await env.collections + .userMessages(clinicianId) + .get() + expect(clinicianMessages.docs).to.have.length(1) + const clinicianMessage = clinicianMessages.docs.at(0)?.data() + expect(clinicianMessage?.type).to.equal(UserMessageType.preAppointment) + expect(clinicianMessage?.reference).to.equal(appointmentRef.path) + expect(clinicianMessage?.completionDate).to.be.undefined }) it('should create a complete a message for a past appointment', async () => { - const userId = await env.createUser({ + const clinicianId = await env.createUser({ + type: UserType.clinician, + organization: 'stanford', + }) + + const patientId = await env.createUser({ type: UserType.patient, organization: 'stanford', + clinician: clinicianId, }) const appointment = FHIRAppointment.create({ - userId, + userId: patientId, status: FHIRAppointmentStatus.proposed, created: advanceDateByDays(new Date(), -3), start: advanceDateByDays(new Date(), -1), durationInMinutes: 60, }) - const appointmentRef = env.collections.userAppointments(userId).doc() + const appointmentRef = env.collections.userAppointments(patientId).doc() await appointmentRef.set(appointment) const message = UserMessage.createPreAppointment({ reference: appointmentRef.path, }) - const messageRef = env.collections.userMessages(userId).doc() - await messageRef.set(message) + const patientMessageRef = env.collections.userMessages(patientId).doc() + await patientMessageRef.set(message) + + const clinicianMessageRef = env.collections + .userMessages(clinicianId) + .doc() + await clinicianMessageRef.set(message) await env.factory.trigger().every15Minutes() - const messages = await env.collections.userMessages(userId).get() - expect(messages.docs).to.have.length(1) + const patientMessages = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages.docs).to.have.length(1) + const patientMessage = patientMessages.docs.at(0)?.data() + expect(patientMessage?.type).to.equal(UserMessageType.preAppointment) + expect(patientMessage?.reference).to.equal(appointmentRef.path) + expect(patientMessage?.completionDate).to.exist - const actualMessage = messages.docs.at(0)?.data() - expect(actualMessage?.type).to.equal(UserMessageType.preAppointment) - expect(actualMessage?.reference).to.equal(appointmentRef.path) - expect(actualMessage?.completionDate).to.exist + const clinicianMessages = await env.collections + .userMessages(patientId) + .get() + expect(clinicianMessages.docs).to.have.length(1) + const clinicianMessage = clinicianMessages.docs.at(0)?.data() + expect(clinicianMessage?.type).to.equal(UserMessageType.preAppointment) + expect(clinicianMessage?.reference).to.equal(appointmentRef.path) + expect(clinicianMessage?.completionDate).to.exist }) }) @@ -138,5 +180,192 @@ describeWithEmulators('TriggerService', (env) => { expect(questionnaireMessage).to.exist expect(questionnaireMessage?.completionDate).to.be.undefined }) + + it('create a message about inactivity', async () => { + const clinicianId = await env.createUser({ + type: UserType.clinician, + organization: 'stanford', + }) + + const patientId = await env.createUser({ + type: UserType.patient, + organization: 'stanford', + clinician: clinicianId, + dateOfEnrollment: advanceDateByDays(new Date(), -2), + lastActiveDate: advanceDateByDays(new Date(), -8), + }) + + await env.factory.trigger().everyMorning() + + const patientMessages = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages.docs).to.have.length(2) + const patientMessagesData = patientMessages.docs.map((doc) => doc.data()) + const vitalsMessage = patientMessagesData + .filter((message) => message.type === UserMessageType.vitals) + .at(0) + expect(vitalsMessage).to.exist + expect(vitalsMessage?.reference).to.be.undefined + expect(vitalsMessage?.completionDate).to.be.undefined + const inactiveMessage = patientMessagesData + .filter((message) => message.type === UserMessageType.vitals) + .at(0) + expect(inactiveMessage).to.exist + expect(inactiveMessage?.reference).to.be.undefined + expect(inactiveMessage?.completionDate).to.be.undefined + + const clinicianMessages = await env.collections + .userMessages(clinicianId) + .get() + expect(clinicianMessages.docs).to.have.length(1) + const clinicianMessage = clinicianMessages.docs.at(0)?.data() + expect(clinicianMessage?.type).to.equal(UserMessageType.inactive) + expect(clinicianMessage?.reference).to.equal(`users/${patientId}`) + expect(clinicianMessage?.completionDate).to.be.undefined + }) + + it('create no message about inactivity', async () => { + const clinicianId = await env.createUser({ + type: UserType.clinician, + organization: 'stanford', + }) + + const patientId = await env.createUser({ + type: UserType.patient, + organization: 'stanford', + clinician: clinicianId, + dateOfEnrollment: advanceDateByDays(new Date(), -2), + lastActiveDate: advanceDateByDays(new Date(), -1), + }) + + await env.factory.trigger().everyMorning() + + const patientMessages = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages.docs).to.have.length(1) + const patientMessage = patientMessages.docs.at(0)?.data() + expect(patientMessage?.type).to.equal(UserMessageType.vitals) + expect(patientMessage?.completionDate).to.be.undefined + + const clinicianMessages = await env.collections + .userMessages(clinicianId) + .get() + expect(clinicianMessages.docs).to.have.length(0) + }) + }) + + describe('userObservationWritten', () => { + describe('bodyWeightObservations', () => { + it('should create a weight gain message for a user', async () => { + const triggerService = env.factory.trigger() + + const clinicianId = await env.createUser({ + type: UserType.clinician, + organization: 'stanford', + }) + + const patientId = await env.createUser({ + type: UserType.patient, + organization: 'stanford', + clinician: clinicianId, + }) + + const observations = Array.from({ length: 10 }, (_, i) => + FHIRObservation.createSimple({ + id: `observation-${i}`, + code: LoincCode.bodyWeight, + value: 200, + unit: QuantityUnit.lbs, + date: advanceDateByDays(new Date(), -i - 1), + }), + ) + + await Promise.all( + observations.map(async (observation) => + env.collections + .userObservations(patientId, UserObservationCollection.bodyWeight) + .doc() + .set(observation), + ), + ) + await triggerService.userObservationWritten( + patientId, + UserObservationCollection.bodyWeight, + ) + const patientMessages0 = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages0.docs).to.have.length(0) + + const clinicianMessages0 = await env.collections + .userMessages(clinicianId) + .get() + expect(clinicianMessages0.docs).to.have.length(0) + + const slightlyHigherWeight = FHIRObservation.createSimple({ + id: 'observation-10', + code: LoincCode.bodyWeight, + value: 207.5, + unit: QuantityUnit.lbs, + date: advanceDateByMinutes(new Date(), -30), + }) + await env.collections + .userObservations(patientId, UserObservationCollection.bodyWeight) + .doc() + .set(slightlyHigherWeight) + await triggerService.userObservationWritten( + patientId, + UserObservationCollection.bodyWeight, + ) + const patientMessages1 = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages1.docs, 'patientMessages1').to.have.length(1) + const patientMessage1 = patientMessages1.docs.at(0)?.data() + expect(patientMessage1?.type).to.equal(UserMessageType.weightGain) + expect(patientMessage1?.completionDate).to.be.undefined + + const clinicianMessages1 = await env.collections + .userMessages(patientId) + .get() + expect(clinicianMessages1.docs, 'clinicianMessages1').to.have.length(1) + const clinicianMessage1 = clinicianMessages1.docs.at(0)?.data() + expect(clinicianMessage1?.type).to.equal(UserMessageType.weightGain) + expect(clinicianMessage1?.completionDate).to.be.undefined + + const actuallyHigherWeight = FHIRObservation.createSimple({ + id: 'observation-11', + code: LoincCode.bodyWeight, + value: 208, + unit: QuantityUnit.lbs, + date: advanceDateByMinutes(new Date(), -15), + }) + await env.collections + .userObservations(patientId, UserObservationCollection.bodyWeight) + .doc() + .set(actuallyHigherWeight) + await triggerService.userObservationWritten( + patientId, + UserObservationCollection.bodyWeight, + ) + const patientMessages2 = await env.collections + .userMessages(patientId) + .get() + expect(patientMessages2.docs, 'patientMessages2').to.have.length(1) + const patientMessage2 = patientMessages1.docs.at(0)?.data() + expect(patientMessage2?.type).to.equal(UserMessageType.weightGain) + expect(patientMessage2?.completionDate).to.be.undefined + + const clinicianMessages2 = await env.collections + .userMessages(patientId) + .get() + expect(clinicianMessages2.docs, 'clinicianMessages2').to.have.length(1) + const clinicianMessage2 = clinicianMessages1.docs.at(0)?.data() + expect(clinicianMessage2?.type).to.equal(UserMessageType.weightGain) + expect(clinicianMessage2?.completionDate).to.be.undefined + }) + }) }) }) diff --git a/functions/src/services/trigger/triggerService.ts b/functions/src/services/trigger/triggerService.ts index 20b86c2e..db5fab53 100644 --- a/functions/src/services/trigger/triggerService.ts +++ b/functions/src/services/trigger/triggerService.ts @@ -44,6 +44,7 @@ export class TriggerService { const tomorrow = advanceDateByDays(now, 1) const yesterday = advanceDateByDays(now, -1) const patientService = this.factory.patient() + const userService = this.factory.user() const messageService = this.factory.message() const upcomingAppointments = await patientService.getEveryAppoinment( @@ -56,15 +57,31 @@ export class TriggerService { ) await Promise.all( - upcomingAppointments.map(async (appointment) => - messageService.addMessage( - appointment.path.split('/')[1], + upcomingAppointments.map(async (appointment) => { + const userId = appointment.path.split('/')[1] + await messageService.addMessage( + userId, UserMessage.createPreAppointment({ reference: appointment.path, }), { notify: true }, - ), - ), + ) + const user = await userService.getUser(userId) + const clinicianId = user?.content.clinician + logger.debug( + `TriggerService.every15Minutes: About to add clinician message for clinician ${clinicianId} and appointment ${appointment.path}.`, + ) + if (clinicianId !== undefined) { + await messageService.addMessage( + clinicianId, + UserMessage.createPreAppointment({ + reference: appointment.path, + isDismissible: true, + }), + { notify: true }, + ) + } + }), ) const pastAppointments = await patientService.getEveryAppoinment( @@ -90,9 +107,10 @@ export class TriggerService { async everyMorning() { const now = new Date() const messageService = this.factory.message() - const users = await this.factory.user().getAllPatients() + const userService = this.factory.user() + const patients = await userService.getAllPatients() - logger.debug(`everyMorning: Found ${users.length} patients`) + logger.debug(`everyMorning: Found ${patients.length} patients`) const symptomReminderMessage = UserMessage.createSymptomQuestionnaire({ questionnaireReference: QuestionnaireReference.enUS, @@ -100,7 +118,7 @@ export class TriggerService { const vitalsMessage = UserMessage.createVitals() await Promise.all( - users.map(async (user) => { + patients.map(async (user) => { try { await messageService.addMessage(user.id, vitalsMessage, { notify: true, @@ -146,6 +164,30 @@ export class TriggerService { } }), ) + + const inactivePatients = patients.filter( + (patient) => advanceDateByDays(patient.content.lastActiveDate, 7) < now, + ) + await Promise.all( + inactivePatients.map(async (user) => { + await messageService.addMessage( + user.id, + UserMessage.createInactive({}), + { notify: true }, + ) + + if (user.content.clinician !== undefined) { + await messageService.addMessage( + user.content.clinician, + UserMessage.createInactive({ + reference: `users/${user.id}`, + isDismissible: true, + }), + { notify: true }, + ) + } + }), + ) } // Methods - Triggers @@ -191,25 +233,10 @@ export class TriggerService { ) const recommendations = await this.updateRecommendationsForUser(userId) - - const hasImprovementAvailable = recommendations.some((recommendation) => - [ - UserMedicationRecommendationType.improvementAvailable, - UserMedicationRecommendationType.notStarted, - ].includes(recommendation.displayInformation.type), - ) - - logger.debug( - `questionnaireResponseWritten(${userId}, ${questionnaireResponseId}): Improvement available: ${hasImprovementAvailable ? 'yes' : 'no'}`, - ) - - if (hasImprovementAvailable) { - await messageService.addMessage( - userId, - UserMessage.createMedicationUptitration(), - { notify: true }, - ) - } + await this.addMedicationUptitrationMessageIfNeeded({ + userId: userId, + recommendations: recommendations, + }) } async userEnrolled(userId: string) { @@ -244,6 +271,15 @@ export class TriggerService { userId: string, collection: UserObservationCollection, ): Promise { + try { + const userService = this.factory.user() + await userService.updateLastActiveDate(userId) + } catch (error) { + logger.error( + `TriggerService.userObservationWritten(${userId}, ${collection}): Updating lastActiveDate failed due to ${String(error)}`, + ) + } + try { await this.updateRecommendationsForUser(userId) } catch (error) { @@ -278,11 +314,26 @@ export class TriggerService { `TriggerService.userObservationWritten(${userId}, ${collection}): Most recent body weight is ${mostRecentBodyWeight} compared to a median of ${bodyWeightMedian}`, ) if (mostRecentBodyWeight - bodyWeightMedian >= 7) { - await this.factory - .message() - .addMessage(userId, UserMessage.createWeightGain(), { - notify: true, - }) + const messageService = this.factory.message() + await messageService.addMessage( + userId, + UserMessage.createWeightGain(), + { notify: true }, + ) + + const userService = this.factory.user() + const user = await userService.getUser(userId) + const clinicianId = user?.content.clinician + + if (clinicianId !== undefined) { + await messageService.addMessage( + clinicianId, + UserMessage.createWeightGain({ + reference: `users/${userId}`, + }), + { notify: true }, + ) + } } } catch (error) { logger.error( @@ -555,15 +606,15 @@ export class TriggerService { private async addMedicationUptitrationMessageIfNeeded(input: { userId: string - recommendations?: UserMedicationRecommendation[] + recommendations: UserMedicationRecommendation[] }): Promise { - const hasImprovementAvailable = - input.recommendations?.some((recommendation) => + const hasImprovementAvailable = input.recommendations.some( + (recommendation) => [ UserMedicationRecommendationType.improvementAvailable, UserMedicationRecommendationType.notStarted, ].includes(recommendation.displayInformation.type), - ) ?? false + ) logger.debug( `TriggerService.addMedicationUptitrationMessageIfNeeded(${input.userId}): Improvement available: ${hasImprovementAvailable ? 'yes' : 'no'}`, @@ -573,6 +624,18 @@ export class TriggerService { const message = UserMessage.createMedicationUptitration() const messageService = this.factory.message() await messageService.addMessage(input.userId, message, { notify: true }) + + const user = await this.factory.user().getUser(input.userId) + const clinicianId = user?.content.clinician + if (clinicianId !== undefined) { + await messageService.addMessage( + clinicianId, + UserMessage.createMedicationUptitration({ + reference: `users/${input.userId}`, + }), + { notify: true }, + ) + } return true } } diff --git a/functions/src/services/user/databaseUserService.ts b/functions/src/services/user/databaseUserService.ts index 9872ae3f..e66def6e 100644 --- a/functions/src/services/user/databaseUserService.ts +++ b/functions/src/services/user/databaseUserService.ts @@ -7,6 +7,7 @@ // import { + dateConverter, type Invitation, type Organization, User, @@ -198,6 +199,7 @@ export class DatabaseUserService implements UserService { collections.users.doc(userId), new User({ ...invitation.content.user, + lastActiveDate: new Date(), invitationCode: invitation.content.code, dateOfEnrollment: new Date(), }), @@ -288,6 +290,14 @@ export class DatabaseUserService implements UserService { ) } + async updateLastActiveDate(userId: string): Promise { + return this.databaseService.runTransaction((collections, transaction) => { + transaction.update(collections.users.doc(userId), { + lastActiveDate: dateConverter.encode(new Date()), + }) + }) + } + async deleteUser(userId: string): Promise { await this.databaseService.bulkWrite(async (collections, writer) => { await collections.firestore.recursiveDelete( diff --git a/functions/src/services/user/userService.mock.ts b/functions/src/services/user/userService.mock.ts index e6784d4d..1be9a72d 100644 --- a/functions/src/services/user/userService.mock.ts +++ b/functions/src/services/user/userService.mock.ts @@ -66,6 +66,7 @@ export class MockUserService implements UserService { dateOfBirth: new Date('1970-01-02'), clinician: 'mockPatient', receivesAppointmentReminders: true, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: true, receivesRecommendationUpdates: true, @@ -92,6 +93,7 @@ export class MockUserService implements UserService { dateOfBirth: new Date('1970-01-02'), clinician: 'mockPatient', receivesAppointmentReminders: true, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: true, receivesRecommendationUpdates: true, @@ -166,7 +168,9 @@ export class MockUserService implements UserService { type: UserType.clinician, dateOfBirth: new Date('1970-01-02'), clinician: 'mockClinician', + lastActiveDate: new Date('2024-04-04'), receivesAppointmentReminders: true, + receivesInactivityReminders: true, receivesMedicationUpdates: true, receivesQuestionnaireReminders: true, receivesRecommendationUpdates: true, @@ -180,6 +184,10 @@ export class MockUserService implements UserService { } } + async updateLastActiveDate(userId: string): Promise { + return + } + async deleteUser(userId: string): Promise { return } diff --git a/functions/src/services/user/userService.ts b/functions/src/services/user/userService.ts index b73915db..57f674ba 100644 --- a/functions/src/services/user/userService.ts +++ b/functions/src/services/user/userService.ts @@ -53,5 +53,6 @@ export interface UserService { getAllPatients(): Promise>> getUser(userId: string): Promise | undefined> + updateLastActiveDate(userId: string): Promise deleteUser(userId: string): Promise } diff --git a/functions/src/tests/functions/testEnvironment.ts b/functions/src/tests/functions/testEnvironment.ts index 6e5701c2..bd34a050 100644 --- a/functions/src/tests/functions/testEnvironment.ts +++ b/functions/src/tests/functions/testEnvironment.ts @@ -129,9 +129,12 @@ export class EmulatorTestEnvironment { options: { type: UserType organization?: string + clinician?: string dateOfEnrollment?: Date + lastActiveDate?: Date invitationCode?: string receivesAppointmentReminders?: boolean + receivesInactivityReminders?: boolean receivesMedicationUpdates?: boolean receivesQuestionnaireReminders?: boolean receivesRecommendationUpdates?: boolean @@ -145,9 +148,13 @@ export class EmulatorTestEnvironment { type: options.type, organization: options.organization, dateOfEnrollment: options.dateOfEnrollment ?? new Date(), + clinician: options.clinician, + lastActiveDate: options.lastActiveDate ?? new Date(), invitationCode: options.invitationCode ?? 'TESTCODE', receivesAppointmentReminders: options.receivesAppointmentReminders ?? true, + receivesInactivityReminders: + options.receivesInactivityReminders ?? true, receivesMedicationUpdates: options.receivesMedicationUpdates ?? true, receivesQuestionnaireReminders: options.receivesQuestionnaireReminders ?? true, diff --git a/functions/src/tests/resources/seeding/users_0_messages.json b/functions/src/tests/resources/seeding/users_0_messages.json index d8d13b62..500f4ec0 100644 --- a/functions/src/tests/resources/seeding/users_0_messages.json +++ b/functions/src/tests/resources/seeding/users_0_messages.json @@ -1,5 +1,20 @@ { "0": { + "creationDate": "2024-06-05T00:00:00.000Z", + "dueDate": null, + "completionDate": null, + "type": "Inactive", + "title": { + "en": "Inactive" + }, + "description": { + "en": "You have been inactive for 7 days. Please log in to continue your care." + }, + "action": null, + "isDismissible": false, + "reference": null + }, + "1": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": null, "completionDate": null, @@ -14,7 +29,7 @@ "isDismissible": true, "reference": "medications/203160" }, - "1": { + "2": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": null, "completionDate": null, @@ -29,7 +44,7 @@ "isDismissible": true, "reference": null }, - "2": { + "3": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": null, "completionDate": null, @@ -44,7 +59,7 @@ "isDismissible": false, "reference": null }, - "3": { + "4": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": null, "completionDate": null, @@ -59,7 +74,7 @@ "isDismissible": false, "reference": null }, - "4": { + "5": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": "2024-06-06T00:00:00.000Z", "completionDate": null, @@ -74,7 +89,7 @@ "isDismissible": false, "reference": null }, - "5": { + "6": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": null, "completionDate": null, @@ -89,7 +104,7 @@ "isDismissible": true, "reference": null }, - "6": { + "7": { "creationDate": "2024-06-05T00:00:00.000Z", "dueDate": null, "completionDate": null,