Auspicious14 commited on
Commit
683d259
·
1 Parent(s): 08512dc

feat(monitoring): add real-time monitoring engine with state management

Browse files

Introduce a comprehensive monitoring engine that evaluates user health data (blood pressure and symptoms) in real-time. The engine determines monitoring states (NORMAL, MONITOR, URGENT, EMERGENCY) based on clinical rules and creates automated follow-up tasks. A scheduler service handles overdue tasks and inactivity reminders. The blood pressure module now integrates with the monitoring engine instead of the legacy care priority system.

- Add new MonitoringEngineModule, service, controller, and scheduler
- Define AppNotificationType enum for notification categorization
- Extend Prisma schema with MonitoringStateRecord and FollowUpTask entities
- Modify BloodPressureController to trigger monitoring evaluation on new readings
- Make sendPushNotification method public for cross-module use
- Remove direct dependency on CarePriorityModule in BloodPressureModule

prisma/schema.prisma CHANGED
@@ -67,6 +67,12 @@ model UserAuth {
67
  // Relation to Notifications
68
  notifications Notification[]
69
 
 
 
 
 
 
 
70
  @@index([email])
71
  @@index([phone])
72
  @@map("user_auth")
@@ -280,3 +286,75 @@ model Notification {
280
  @@index([createdAt])
281
  @@map("notification")
282
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  // Relation to Notifications
68
  notifications Notification[]
69
 
70
+ // Relation to MonitoringStateRecord
71
+ monitoringStateRecord MonitoringStateRecord?
72
+
73
+ // Relation to FollowUpTasks
74
+ followUpTasks FollowUpTask[]
75
+
76
  @@index([email])
77
  @@index([phone])
78
  @@map("user_auth")
 
286
  @@index([createdAt])
287
  @@map("notification")
288
  }
289
+
290
+ /**
291
+ * MonitoringState Enum
292
+ *
293
+ * Defines the possible states of the monitoring system for a user.
294
+ */
295
+ enum MonitoringState {
296
+ NORMAL
297
+ MONITOR
298
+ URGENT
299
+ EMERGENCY
300
+ }
301
+
302
+ /**
303
+ * MonitoringStateRecord Entity
304
+ *
305
+ * Stores the current monitoring state for each user.
306
+ */
307
+ model MonitoringStateRecord {
308
+ id String @id @default(uuid())
309
+ userId String @unique
310
+ currentState MonitoringState
311
+ lastUpdatedAt DateTime @default(now())
312
+ reason String?
313
+
314
+ userAuth UserAuth @relation(fields: [userId], references: [id], onDelete: Cascade)
315
+
316
+ @@index([userId])
317
+ @@map("monitoring_state_record")
318
+ }
319
+
320
+ /**
321
+ * FollowUpTaskType Enum
322
+ *
323
+ * Defines the types of follow-up tasks.
324
+ */
325
+ enum FollowUpTaskType {
326
+ RECHECK
327
+ SEEK_CARE
328
+ }
329
+
330
+ /**
331
+ * FollowUpTaskStatus Enum
332
+ *
333
+ * Defines the status of follow-up tasks.
334
+ */
335
+ enum FollowUpTaskStatus {
336
+ PENDING
337
+ COMPLETED
338
+ MISSED
339
+ }
340
+
341
+ /**
342
+ * FollowUpTask Entity
343
+ *
344
+ * Stores details about each follow-up task.
345
+ */
346
+ model FollowUpTask {
347
+ id String @id @default(uuid())
348
+ userId String
349
+ type FollowUpTaskType
350
+ dueAt DateTime
351
+ status FollowUpTaskStatus @default(PENDING)
352
+ relatedReadingId String? // Optional: Link to a specific BP reading that triggered the task
353
+
354
+ userAuth UserAuth @relation(fields: [userId], references: [id], onDelete: Cascade)
355
+
356
+ @@index([userId])
357
+ @@index([dueAt])
358
+ @@index([status])
359
+ @@map("follow_up_task")
360
+ }
src/app.module.ts CHANGED
@@ -12,6 +12,7 @@ import { CarePriorityModule } from "./care-priority/care-priority.module";
12
  import { NotificationsModule } from "./notifications/notifications.module";
13
  import { EducationModule } from "./education/education.module";
14
  import { ClinicFinderModule } from "./clinic-finder/clinic-finder.module";
 
15
  import { AppController } from "./app.controller";
16
  import { HealthController } from "./health.controller";
17
 
@@ -46,6 +47,7 @@ import { HealthController } from "./health.controller";
46
  NotificationsModule,
47
  EducationModule,
48
  ClinicFinderModule,
 
49
  ],
50
  controllers: [AppController, HealthController],
51
  providers: [
 
12
  import { NotificationsModule } from "./notifications/notifications.module";
13
  import { EducationModule } from "./education/education.module";
14
  import { ClinicFinderModule } from "./clinic-finder/clinic-finder.module";
15
+ import { MonitoringEngineModule } from "./monitoring-engine/monitoring-engine.module";
16
  import { AppController } from "./app.controller";
17
  import { HealthController } from "./health.controller";
18
 
 
47
  NotificationsModule,
48
  EducationModule,
49
  ClinicFinderModule,
50
+ MonitoringEngineModule,
51
  ],
52
  controllers: [AppController, HealthController],
53
  providers: [
src/blood-pressure/blood-pressure.controller.ts CHANGED
@@ -17,15 +17,14 @@ import {
17
  import { BloodPressureService } from "./blood-pressure.service";
18
  import { CreateBloodPressureDto } from "./dto/create-blood-pressure.dto";
19
  import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
20
- import { HealthAssessmentService } from "../care-priority/health-assessment.service";
21
 
22
  @Controller("blood-pressure")
23
  @UseGuards(JwtAuthGuard)
24
  export class BloodPressureController {
25
  constructor(
26
  private readonly bloodPressureService: BloodPressureService,
27
- @Inject(forwardRef(() => HealthAssessmentService))
28
- private readonly healthAssessmentService: HealthAssessmentService
29
  ) {}
30
 
31
  @Post()
@@ -40,10 +39,15 @@ export class BloodPressureController {
40
  createBloodPressureDto
41
  );
42
 
43
- // Trigger health assessment and notifications (don't await to avoid blocking response)
44
- this.healthAssessmentService.assessAndNotify(userId);
45
 
46
- return reading;
 
 
 
 
 
 
47
  }
48
 
49
  /**
 
17
  import { BloodPressureService } from "./blood-pressure.service";
18
  import { CreateBloodPressureDto } from "./dto/create-blood-pressure.dto";
19
  import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
20
+ import { MonitoringEngineService } from "../monitoring-engine/monitoring-engine.service";
21
 
22
  @Controller("blood-pressure")
23
  @UseGuards(JwtAuthGuard)
24
  export class BloodPressureController {
25
  constructor(
26
  private readonly bloodPressureService: BloodPressureService,
27
+ private readonly monitoringEngineService: MonitoringEngineService,
 
28
  ) {}
29
 
30
  @Post()
 
39
  createBloodPressureDto
40
  );
41
 
42
+ const monitoringResult = await this.monitoringEngineService.evaluate(userId);
 
43
 
44
+ return {
45
+ reading,
46
+ monitoringState: monitoringResult.state,
47
+ message: monitoringResult.message,
48
+ nextAction: monitoringResult.nextAction,
49
+ followUp: monitoringResult.followUp,
50
+ };
51
  }
52
 
53
  /**
src/blood-pressure/blood-pressure.module.ts CHANGED
@@ -1,25 +1,10 @@
1
  import { Module, forwardRef } from "@nestjs/common";
2
  import { BloodPressureController } from "./blood-pressure.controller";
3
  import { BloodPressureService } from "./blood-pressure.service";
4
- import { CarePriorityModule } from "../care-priority/care-priority.module";
5
-
6
- /**
7
- * Blood Pressure Module
8
- *
9
- * RESPONSIBILITIES:
10
- * - Store BP readings (systolic/diastolic)
11
- * - Manual entry only
12
- * - Timestamp tracking
13
- *
14
- * CLINICAL SAFETY CONSTRAINTS:
15
- * - Do NOT label readings as normal/abnormal
16
- * - Do NOT store interpretations
17
- * - Neutral data handling only
18
- * - Raw measurements for care priority engine
19
- */
20
 
21
  @Module({
22
- imports: [forwardRef(() => CarePriorityModule)],
23
  controllers: [BloodPressureController],
24
  providers: [BloodPressureService],
25
  exports: [BloodPressureService],
 
1
  import { Module, forwardRef } from "@nestjs/common";
2
  import { BloodPressureController } from "./blood-pressure.controller";
3
  import { BloodPressureService } from "./blood-pressure.service";
4
+ import { MonitoringEngineModule } from "../monitoring-engine/monitoring-engine.module";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  @Module({
7
+ imports: [MonitoringEngineModule],
8
  controllers: [BloodPressureController],
9
  providers: [BloodPressureService],
10
  exports: [BloodPressureService],
src/common/enums/app-notification-type.enum.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export enum AppNotificationType {
2
+ BP_REMINDER = 'BP_REMINDER',
3
+ INACTIVITY_REMINDER = 'INACTIVITY_REMINDER',
4
+ URGENT_INACTIVITY_REMINDER = 'URGENT_INACTIVITY_REMINDER',
5
+ FOLLOW_UP_RECHECK = 'FOLLOW_UP_RECHECK',
6
+ FOLLOW_UP_SEEK_CARE = 'FOLLOW_UP_SEEK_CARE',
7
+ FOLLOW_UP_MISSED = 'FOLLOW_UP_MISSED',
8
+ TREND_ALERT = 'TREND_ALERT',
9
+ ESCALATION_ALERT = 'ESCALATION_ALERT',
10
+ SESSION_EXPIRY = 'SESSION_EXPIRY',
11
+ GENERIC = 'GENERIC',
12
+ }
src/monitoring-engine/monitoring-engine.controller.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Post,
5
+ Param,
6
+ UseGuards,
7
+ Request,
8
+ HttpCode,
9
+ HttpStatus,
10
+ } from '@nestjs/common';
11
+ import { MonitoringEngineService } from './monitoring-engine.service';
12
+ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
13
+
14
+ @Controller('monitoring-engine')
15
+ @UseGuards(JwtAuthGuard)
16
+ export class MonitoringEngineController {
17
+ constructor(
18
+ private readonly monitoringEngineService: MonitoringEngineService,
19
+ ) {}
20
+
21
+ @Get('state')
22
+ @HttpCode(HttpStatus.OK)
23
+ async getMonitoringState(@Request() req: any) {
24
+ const userId = req.user.id;
25
+ const state = await this.monitoringEngineService.getMonitoringState(userId);
26
+ return state;
27
+ }
28
+
29
+ @Get('follow-ups')
30
+ @HttpCode(HttpStatus.OK)
31
+ async getPendingFollowUps(@Request() req: any) {
32
+ const userId = req.user.id;
33
+ const followUps = await this.monitoringEngineService.getPendingFollowUpTasks(
34
+ userId,
35
+ );
36
+ return followUps;
37
+ }
38
+
39
+ @Post('follow-ups/:id/complete')
40
+ @HttpCode(HttpStatus.OK)
41
+ async completeFollowUp(@Request() req: any, @Param('id') id: string) {
42
+ const userId = req.user.id; // Ensure the user owns the task
43
+ // In a real application, you'd add a check here to ensure the task belongs to the user
44
+ const completedTask = await this.monitoringEngineService.completeFollowUpTask(
45
+ id,
46
+ );
47
+ return completedTask;
48
+ }
49
+ }
src/monitoring-engine/monitoring-engine.module.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { MonitoringEngineService } from './monitoring-engine.service';
3
+ import { MonitoringEngineController } from './monitoring-engine.controller';
4
+ import { PrismaService } from '../database/prisma.service';
5
+ import { BloodPressureService } from '../blood-pressure/blood-pressure.service';
6
+ import { SymptomsService } from '../symptoms/symptoms.service';
7
+ import { UserProfileService } from '../user-profile/user-profile.service';
8
+ import { MonitoringSchedulerService } from './monitoring-scheduler.service';
9
+ import { NotificationsModule } from '../notifications/notifications.module';
10
+ import { UserProfileModule } from '../user-profile/user-profile.module';
11
+ import { ScheduleModule } from '@nestjs/schedule';
12
+
13
+ @Module({
14
+ imports: [NotificationsModule, UserProfileModule, ScheduleModule.forRoot()],
15
+ providers: [
16
+ MonitoringEngineService,
17
+ PrismaService,
18
+ BloodPressureService,
19
+ SymptomsService,
20
+ UserProfileService,
21
+ MonitoringSchedulerService,
22
+ ],
23
+ controllers: [MonitoringEngineController],
24
+ exports: [MonitoringEngineService],
25
+ })
26
+ export class MonitoringEngineModule {}
src/monitoring-engine/monitoring-engine.service.ts ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { PrismaService } from '../database/prisma.service';
3
+ import { BloodPressureService } from '../blood-pressure/blood-pressure.service';
4
+ import { SymptomsService } from '../symptoms/symptoms.service';
5
+ import { UserProfileService } from '../user-profile/user-profile.service';
6
+ import {
7
+ MonitoringState,
8
+ FollowUpTaskType,
9
+ FollowUpTaskStatus,
10
+ SymptomType,
11
+ } from '@prisma/client';
12
+
13
+ @Injectable()
14
+ export class MonitoringEngineService {
15
+ private readonly logger = new Logger(MonitoringEngineService.name);
16
+
17
+ constructor(
18
+ private readonly prisma: PrismaService,
19
+ private readonly bloodPressureService: BloodPressureService,
20
+ private readonly symptomsService: SymptomsService,
21
+ private readonly userProfileService: UserProfileService,
22
+ ) {}
23
+
24
+ async evaluate(userId: string) {
25
+ const now = new Date();
26
+ let currentState: MonitoringState = MonitoringState.NORMAL;
27
+ let reason: string | null = null;
28
+ let nextAction: string | null = null;
29
+ let followUpTask = null;
30
+
31
+ // 1. Aggregate historical data
32
+ const last48Hours = new Date(now.getTime() - 48 * 60 * 60 * 1000);
33
+ const last72Hours = new Date(now.getTime() - 72 * 60 * 60 * 1000);
34
+
35
+ const recentBPs = await this.bloodPressureService.getRecentReadings(
36
+ userId,
37
+ 48,
38
+ );
39
+ const latestBP = recentBPs.length > 0 ? recentBPs[0] : null;
40
+ const recentSymptoms = await this.symptomsService.getRecentSymptoms(
41
+ userId,
42
+ 72,
43
+ );
44
+ const symptomTypes = new Set(recentSymptoms.map((s) => s.symptomType));
45
+
46
+ // 2. Detect patterns (TREND ENGINE)
47
+ // Rule B: Severe BP
48
+ if (latestBP && (latestBP.systolic >= 160 || latestBP.diastolic >= 110)) {
49
+ currentState = MonitoringState.EMERGENCY;
50
+ reason = 'Severe blood pressure reading detected.';
51
+ nextAction = 'Seek immediate medical attention.';
52
+ }
53
+
54
+ // Rule C: BP + symptom combination
55
+ if (
56
+ latestBP &&
57
+ (latestBP.systolic >= 140 || latestBP.diastolic >= 90) &&
58
+ (symptomTypes.has(SymptomType.HEADACHE) ||
59
+ symptomTypes.has(SymptomType.BLURRED_VISION) ||
60
+ symptomTypes.has(SymptomType.UPPER_ABDOMINAL_PAIN))
61
+ ) {
62
+ currentState = MonitoringState.EMERGENCY;
63
+ reason = 'Elevated blood pressure with severe symptoms.';
64
+ nextAction = 'Seek immediate medical attention.';
65
+ }
66
+
67
+ // If already EMERGENCY, we stop here for escalation
68
+ if (currentState === MonitoringState.EMERGENCY) {
69
+ await this.updateMonitoringState(userId, currentState, reason);
70
+ return {
71
+ state: currentState,
72
+ message: reason,
73
+ nextAction,
74
+ followUp: null,
75
+ };
76
+ }
77
+
78
+ // Rule A: Repeated high BP (>=3 readings where systolic >=140 OR diastolic >=90 within 48h)
79
+ const elevatedReadings = recentBPs.filter(
80
+ (bp) => bp.systolic >= 140 || bp.diastolic >= 90,
81
+ );
82
+ if (elevatedReadings.length >= 3) {
83
+ currentState = MonitoringState.URGENT;
84
+ reason = 'Repeated high blood pressure readings within 48 hours.';
85
+ nextAction = 'Contact your healthcare provider within 24 hours.';
86
+ }
87
+
88
+ // Rule D: Rising trend (basic version) - last 3 readings show increasing systolic or diastolic
89
+ if (recentBPs.length >= 3) {
90
+ const sortedBPs = recentBPs
91
+ .slice(0, 3)
92
+ .sort((a, b) => a.recordedAt.getTime() - b.recordedAt.getTime()); // Oldest first
93
+
94
+ const isSystolicRising =
95
+ sortedBPs[0].systolic < sortedBPs[1].systolic &&
96
+ sortedBPs[1].systolic < sortedBPs[2].systolic;
97
+ const isDiastolicRising =
98
+ sortedBPs[0].diastolic < sortedBPs[1].diastolic &&
99
+ sortedBPs[1].diastolic < sortedBPs[2].diastolic;
100
+
101
+ if (isSystolicRising || isDiastolicRising) {
102
+ // Cast to avoid TS narrowing issues when comparing against the "current" state
103
+ const checkState = currentState as MonitoringState;
104
+ if (checkState === MonitoringState.NORMAL) {
105
+ currentState = MonitoringState.MONITOR;
106
+ reason = 'Rising trend in blood pressure detected.';
107
+ nextAction = 'Continue monitoring closely.';
108
+ } else if (checkState === MonitoringState.MONITOR) {
109
+ currentState = MonitoringState.URGENT;
110
+ reason = 'Significant rising trend in blood pressure detected.';
111
+ nextAction = 'Contact your healthcare provider within 24 hours.';
112
+ }
113
+ }
114
+ }
115
+
116
+ // Initial high reading (NORMAL -> MONITOR)
117
+ if (
118
+ (currentState as MonitoringState) === MonitoringState.NORMAL &&
119
+ latestBP &&
120
+ (latestBP.systolic >= 140 || latestBP.diastolic >= 90)
121
+ ) {
122
+ currentState = MonitoringState.MONITOR;
123
+ reason = 'First elevated blood pressure reading.';
124
+ nextAction = 'Recheck your blood pressure in 4 hours.';
125
+ }
126
+
127
+ // 3. Introduce STATE MACHINE & 4. STATE TRANSITIONS
128
+ const previousStateRecord = await this.prisma.monitoringStateRecord.findUnique(
129
+ {
130
+ where: { userId },
131
+ },
132
+ );
133
+
134
+ if (previousStateRecord) {
135
+ // Transition from Any -> NORMAL (after consistent normal readings)
136
+ // This is a simplified check. A more robust solution would involve checking a longer history of normal readings.
137
+ const checkState = currentState as MonitoringState;
138
+ if (
139
+ previousStateRecord.currentState !== MonitoringState.NORMAL &&
140
+ checkState === MonitoringState.NORMAL &&
141
+ recentBPs.every((bp) => bp.systolic < 140) &&
142
+ recentBPs.every((bp) => bp.diastolic < 90) &&
143
+ recentSymptoms.length === 0
144
+ ) {
145
+ currentState = MonitoringState.NORMAL;
146
+ reason = 'Consistent normal readings and no concerning symptoms.';
147
+ nextAction = 'Continue routine monitoring.';
148
+ }
149
+ }
150
+
151
+ // 5. FOLLOW-UP SYSTEM (CRITICAL)
152
+ // When BP >= 140/90: Create RECHECK task: dueAt = now + 4 hours
153
+ if (
154
+ latestBP &&
155
+ (latestBP.systolic >= 140 || latestBP.diastolic >= 90) &&
156
+ (currentState as MonitoringState) !== MonitoringState.EMERGENCY
157
+ ) {
158
+ followUpTask = await this.createFollowUpTask(
159
+ userId,
160
+ FollowUpTaskType.RECHECK,
161
+ new Date(now.getTime() + 4 * 60 * 60 * 1000), // 4 hours from now
162
+ latestBP.id,
163
+ );
164
+ nextAction = 'Please recheck your blood pressure in 4 hours.';
165
+ }
166
+
167
+ // When state = URGENT: Create SEEK_CARE task: dueAt = now + 24 hours
168
+ if ((currentState as MonitoringState) === MonitoringState.URGENT) {
169
+ followUpTask = await this.createFollowUpTask(
170
+ userId,
171
+ FollowUpTaskType.SEEK_CARE,
172
+ new Date(now.getTime() + 24 * 60 * 60 * 1000), // 24 hours from now
173
+ latestBP?.id, // Link to the latest BP if available
174
+ );
175
+ nextAction = 'Contact your healthcare provider within 24 hours.';
176
+ }
177
+
178
+ await this.updateMonitoringState(userId, currentState, reason);
179
+
180
+ return {
181
+ state: currentState,
182
+ message: reason,
183
+ nextAction,
184
+ followUp: followUpTask,
185
+ };
186
+ }
187
+
188
+ async getMonitoringState(userId: string) {
189
+ return this.prisma.monitoringStateRecord.findUnique({
190
+ where: { userId },
191
+ });
192
+ }
193
+
194
+ async getPendingFollowUpTasks(userId: string) {
195
+ return this.prisma.followUpTask.findMany({
196
+ where: {
197
+ userId,
198
+ status: FollowUpTaskStatus.PENDING,
199
+ },
200
+ orderBy: {
201
+ dueAt: 'asc',
202
+ },
203
+ });
204
+ }
205
+
206
+ async completeFollowUpTask(taskId: string) {
207
+ return this.prisma.followUpTask.update({
208
+ where: { id: taskId },
209
+ data: { status: FollowUpTaskStatus.COMPLETED },
210
+ });
211
+ }
212
+
213
+ private async updateMonitoringState(
214
+ userId: string,
215
+ newState: MonitoringState,
216
+ reason: string | null,
217
+ ) {
218
+ return this.prisma.monitoringStateRecord.upsert({
219
+ where: { userId },
220
+ update: {
221
+ currentState: newState,
222
+ reason,
223
+ lastUpdatedAt: new Date(),
224
+ },
225
+ create: {
226
+ userId,
227
+ currentState: newState,
228
+ reason,
229
+ lastUpdatedAt: new Date(),
230
+ },
231
+ });
232
+ }
233
+
234
+ private async createFollowUpTask(
235
+ userId: string,
236
+ type: FollowUpTaskType,
237
+ dueAt: Date,
238
+ relatedReadingId?: string,
239
+ ) {
240
+ // Check for existing pending tasks of the same type to avoid duplicates
241
+ const existingTask = await this.prisma.followUpTask.findFirst({
242
+ where: {
243
+ userId,
244
+ type,
245
+ status: FollowUpTaskStatus.PENDING,
246
+ },
247
+ });
248
+
249
+ if (existingTask) {
250
+ // Update existing task if dueAt is earlier, or if it's a RECHECK and relatedReadingId is different
251
+ if (
252
+ existingTask.dueAt > dueAt ||
253
+ (type === FollowUpTaskType.RECHECK &&
254
+ existingTask.relatedReadingId !== relatedReadingId)
255
+ ) {
256
+ return this.prisma.followUpTask.update({
257
+ where: { id: existingTask.id },
258
+ data: { dueAt, relatedReadingId },
259
+ });
260
+ }
261
+ return existingTask; // Return existing task if no update is needed
262
+ }
263
+
264
+ return this.prisma.followUpTask.create({
265
+ data: {
266
+ userId,
267
+ type,
268
+ dueAt,
269
+ relatedReadingId,
270
+ },
271
+ });
272
+ }
273
+ }
src/monitoring-engine/monitoring-scheduler.service.ts ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { Cron, CronExpression } from '@nestjs/schedule';
3
+ import { PrismaService } from '../database/prisma.service';
4
+ import { MonitoringEngineService } from './monitoring-engine.service';
5
+ import {
6
+ FollowUpTaskStatus,
7
+ FollowUpTaskType,
8
+ MonitoringState,
9
+ } from '@prisma/client';
10
+ import { NotificationsService } from '../notifications/notifications.service';
11
+ import { UserProfileService } from '../user-profile/user-profile.service';
12
+ import { AppNotificationType } from '../common/enums/app-notification-type.enum';
13
+
14
+ @Injectable()
15
+ export class MonitoringSchedulerService {
16
+ private readonly logger = new Logger(MonitoringSchedulerService.name);
17
+
18
+ constructor(
19
+ private readonly prisma: PrismaService,
20
+ private readonly monitoringEngineService: MonitoringEngineService,
21
+ private readonly notificationsService: NotificationsService,
22
+ private readonly userProfileService: UserProfileService,
23
+ ) {}
24
+
25
+ @Cron(CronExpression.EVERY_HOUR)
26
+ async handleOverdueFollowUps() {
27
+ this.logger.log('Checking for overdue follow-up tasks...');
28
+ const now = new Date();
29
+
30
+ const overdueTasks = await this.prisma.followUpTask.findMany({
31
+ where: {
32
+ dueAt: { lt: now },
33
+ status: FollowUpTaskStatus.PENDING,
34
+ },
35
+ });
36
+
37
+ for (const task of overdueTasks) {
38
+ await this.prisma.followUpTask.update({
39
+ where: { id: task.id },
40
+ data: { status: FollowUpTaskStatus.MISSED },
41
+ });
42
+ this.logger.warn(
43
+ `Follow-up task ${task.id} for user ${task.userId} missed.`,
44
+ );
45
+
46
+ // Trigger escalation (re-evaluate monitoring state)
47
+ const result = await this.monitoringEngineService.evaluate(task.userId);
48
+ this.logger.log(
49
+ `Escalated state for user ${task.userId} due to missed follow-up: ${result.state}`,
50
+ );
51
+
52
+ // Trigger notification
53
+ await this.notificationsService.sendPushNotification(
54
+ task.userId,
55
+ 'Follow-up Missed',
56
+ `Your ${task.type} follow-up task was missed. Current state: ${result.state}.`,
57
+ { type: AppNotificationType.FOLLOW_UP_MISSED },
58
+ );
59
+ }
60
+ this.logger.log(
61
+ `Finished checking overdue follow-up tasks. ${overdueTasks.length} tasks updated.`,
62
+ );
63
+ }
64
+
65
+ @Cron('0 9 * * *') // Every day at 9 AM
66
+ async handleInactivity() {
67
+ this.logger.log('Checking for user inactivity...');
68
+ const now = new Date();
69
+ const fiveDaysAgo = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000);
70
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
71
+
72
+ const users = await this.prisma.userAuth.findMany({
73
+ select: {
74
+ id: true,
75
+ bloodPressureReadings: {
76
+ orderBy: { recordedAt: 'desc' },
77
+ take: 1,
78
+ },
79
+ },
80
+ });
81
+
82
+ for (const user of users) {
83
+ const lastBpReading = user.bloodPressureReadings[0];
84
+ if (!lastBpReading) {
85
+ // No BP ever logged, send initial reminder after 5 days of account creation
86
+ const userProfile = await this.userProfileService.findByUserId(user.id);
87
+ if (userProfile && userProfile.createdAt < fiveDaysAgo) {
88
+ await this.notificationsService.sendPushNotification(
89
+ user.id,
90
+ 'BP Log Reminder',
91
+ "You haven't logged your blood pressure yet. Please log your first reading.",
92
+ { type: AppNotificationType.INACTIVITY_REMINDER },
93
+ );
94
+ this.logger.log(
95
+ `Inactivity reminder sent to user ${user.id} (no BP ever logged).`,
96
+ );
97
+ }
98
+ continue;
99
+ }
100
+
101
+ if (lastBpReading.recordedAt < sevenDaysAgo) {
102
+ // No BP logged in >7 days -> escalate messaging urgency
103
+ await this.notificationsService.sendPushNotification(
104
+ user.id,
105
+ 'Urgent BP Reminder',
106
+ "It's been over 7 days since your last blood pressure reading. Please log it now.",
107
+ { type: AppNotificationType.URGENT_INACTIVITY_REMINDER },
108
+ );
109
+ this.logger.warn(
110
+ `Urgent inactivity reminder sent to user ${user.id} (>7 days).`,
111
+ );
112
+ } else if (lastBpReading.recordedAt < fiveDaysAgo) {
113
+ // No BP logged in >5 days -> send reminder
114
+ await this.notificationsService.sendPushNotification(
115
+ user.id,
116
+ 'BP Log Reminder',
117
+ "It's been over 5 days since your last blood pressure reading. Please log it soon.",
118
+ { type: AppNotificationType.INACTIVITY_REMINDER },
119
+ );
120
+ this.logger.log(
121
+ `Inactivity reminder sent to user ${user.id} (>5 days).`,
122
+ );
123
+ }
124
+ }
125
+ this.logger.log('Finished checking for user inactivity.');
126
+ }
127
+ }
src/notifications/notifications.service.ts CHANGED
@@ -64,7 +64,7 @@ export class NotificationsService {
64
  /**
65
  * Internal helper to send push notifications
66
  */
67
- private async sendPushNotification(
68
  userId: string,
69
  title: string,
70
  body: string,
 
64
  /**
65
  * Internal helper to send push notifications
66
  */
67
+ public async sendPushNotification(
68
  userId: string,
69
  title: string,
70
  body: string,