Auspicious14 commited on
Commit
a8daede
·
1 Parent(s): 1687bb1

feat(notifications): integrate health assessment and enhance email service

Browse files

Add immediate health assessment trigger after blood pressure reading creation
to enable real-time care priority notifications. Enhance email service with
connection pooling and improved error handling. Update notification service
to include detailed push notification logging and standardized formatting.

src/blood-pressure/blood-pressure.controller.ts CHANGED
@@ -18,6 +18,7 @@ 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)
@@ -25,21 +26,27 @@ export class BloodPressureController {
25
  constructor(
26
  private readonly bloodPressureService: BloodPressureService,
27
  private readonly monitoringEngineService: MonitoringEngineService,
 
 
28
  ) {}
29
 
30
  @Post()
31
  @HttpCode(HttpStatus.CREATED)
32
  async create(
33
  @Request() req: any,
34
- @Body() createBloodPressureDto: CreateBloodPressureDto
35
  ) {
36
  const userId = req.user.id;
37
  const reading = await this.bloodPressureService.create(
38
  userId,
39
- createBloodPressureDto
40
  );
41
 
42
- const monitoringResult = await this.monitoringEngineService.evaluate(userId);
 
 
 
 
43
 
44
  return {
45
  reading,
@@ -61,7 +68,7 @@ export class BloodPressureController {
61
  @HttpCode(HttpStatus.OK)
62
  async getReadings(
63
  @Request() req: any,
64
- @Query("limit", new ParseIntPipe({ optional: true })) limit?: number
65
  ) {
66
  const userId = req.user.id;
67
  return this.bloodPressureService.findByUserId(userId, limit);
 
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
+ import { HealthAssessmentService } from "../care-priority/health-assessment.service";
22
 
23
  @Controller("blood-pressure")
24
  @UseGuards(JwtAuthGuard)
 
26
  constructor(
27
  private readonly bloodPressureService: BloodPressureService,
28
  private readonly monitoringEngineService: MonitoringEngineService,
29
+ @Inject(forwardRef(() => HealthAssessmentService))
30
+ private readonly healthAssessmentService: HealthAssessmentService,
31
  ) {}
32
 
33
  @Post()
34
  @HttpCode(HttpStatus.CREATED)
35
  async create(
36
  @Request() req: any,
37
+ @Body() createBloodPressureDto: CreateBloodPressureDto,
38
  ) {
39
  const userId = req.user.id;
40
  const reading = await this.bloodPressureService.create(
41
  userId,
42
+ createBloodPressureDto,
43
  );
44
 
45
+ const monitoringResult =
46
+ await this.monitoringEngineService.evaluate(userId);
47
+
48
+ // Trigger health assessment and notifications (immediate Email/Push if needed)
49
+ this.healthAssessmentService.assessAndNotify(userId);
50
 
51
  return {
52
  reading,
 
68
  @HttpCode(HttpStatus.OK)
69
  async getReadings(
70
  @Request() req: any,
71
+ @Query("limit", new ParseIntPipe({ optional: true })) limit?: number,
72
  ) {
73
  const userId = req.user.id;
74
  return this.bloodPressureService.findByUserId(userId, limit);
src/blood-pressure/blood-pressure.module.ts CHANGED
@@ -2,9 +2,10 @@ 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],
 
2
  import { BloodPressureController } from "./blood-pressure.controller";
3
  import { BloodPressureService } from "./blood-pressure.service";
4
  import { MonitoringEngineModule } from "../monitoring-engine/monitoring-engine.module";
5
+ import { CarePriorityModule } from "../care-priority/care-priority.module";
6
 
7
  @Module({
8
+ imports: [MonitoringEngineModule, forwardRef(() => CarePriorityModule)],
9
  controllers: [BloodPressureController],
10
  providers: [BloodPressureService],
11
  exports: [BloodPressureService],
src/notifications/email.service.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Injectable, Logger } from '@nestjs/common';
2
- import { ConfigService } from '@nestjs/config';
3
- import * as nodemailer from 'nodemailer';
4
 
5
  @Injectable()
6
  export class EmailService {
@@ -9,20 +9,31 @@ export class EmailService {
9
 
10
  constructor(private readonly configService: ConfigService) {
11
  this.transporter = nodemailer.createTransport({
12
- host: this.configService.get('SMTP_HOST'),
13
- port: this.configService.get('SMTP_PORT'),
14
- secure: false, // true for 465, false for other ports
 
 
 
 
 
 
15
  auth: {
16
- user: this.configService.get('SMTP_USER'),
17
- pass: this.configService.get('SMTP_PASS'),
18
  },
19
  });
20
  }
21
 
22
- async sendEmail(to: string, subject: string, text: string, html?: string): Promise<boolean> {
 
 
 
 
 
23
  try {
24
- const from = this.configService.get('SMTP_FROM');
25
-
26
  const info = await this.transporter.sendMail({
27
  from,
28
  to,
@@ -34,7 +45,10 @@ export class EmailService {
34
  this.logger.log(`Email sent successfully: ${info.messageId} to ${to}`);
35
  return true;
36
  } catch (error: any) {
37
- this.logger.error(`Failed to send email to ${to}: ${error.message}`, error.stack);
 
 
 
38
  return false;
39
  }
40
  }
 
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { ConfigService } from "@nestjs/config";
3
+ import * as nodemailer from "nodemailer";
4
 
5
  @Injectable()
6
  export class EmailService {
 
9
 
10
  constructor(private readonly configService: ConfigService) {
11
  this.transporter = nodemailer.createTransport({
12
+ host: this.configService.get("SMTP_HOST"),
13
+ port: this.configService.get("SMTP_PORT"),
14
+ secure: this.configService.get("SMTP_SECURE") === "true",
15
+ pool: true,
16
+ maxConnections: 5,
17
+ maxMessages: 100,
18
+ connectionTimeout: 10000, // 10 seconds
19
+ greetingTimeout: 10000,
20
+ socketTimeout: 30000,
21
  auth: {
22
+ user: this.configService.get("SMTP_USER"),
23
+ pass: this.configService.get("SMTP_PASS"),
24
  },
25
  });
26
  }
27
 
28
+ async sendEmail(
29
+ to: string,
30
+ subject: string,
31
+ text: string,
32
+ html?: string,
33
+ ): Promise<boolean> {
34
  try {
35
+ const from = this.configService.get("SMTP_FROM");
36
+
37
  const info = await this.transporter.sendMail({
38
  from,
39
  to,
 
45
  this.logger.log(`Email sent successfully: ${info.messageId} to ${to}`);
46
  return true;
47
  } catch (error: any) {
48
+ this.logger.error(
49
+ `Failed to send email to ${to}: ${error.message}`,
50
+ error.stack,
51
+ );
52
  return false;
53
  }
54
  }
src/notifications/notifications.service.ts CHANGED
@@ -36,7 +36,7 @@ export class NotificationsService {
36
  constructor(
37
  private readonly prisma: PrismaService,
38
  private readonly emailService: EmailService,
39
- private readonly configService: ConfigService
40
  ) {}
41
 
42
  /**
@@ -68,7 +68,7 @@ export class NotificationsService {
68
  userId: string,
69
  title: string,
70
  body: string,
71
- data?: any
72
  ): Promise<void> {
73
  const user = await this.prisma.userAuth.findUnique({
74
  where: { id: userId },
@@ -76,13 +76,17 @@ export class NotificationsService {
76
  });
77
 
78
  if (!user?.pushToken) {
79
- this.logger.debug(`No push token found for user ${userId}, skipping push.`);
 
 
80
  return;
81
  }
82
 
83
  const { Expo } = await import("expo-server-sdk");
84
  if (!Expo.isExpoPushToken(user.pushToken)) {
85
- this.logger.error(`Push token ${user.pushToken} is not a valid Expo push token`);
 
 
86
  return;
87
  }
88
 
@@ -100,11 +104,17 @@ export class NotificationsService {
100
  const expo = await this.getExpo();
101
  const chunks = expo.chunkPushNotifications(messages);
102
  for (const chunk of chunks) {
103
- await expo.sendPushNotificationsAsync(chunk);
 
 
 
104
  }
105
  this.logger.log(`Push notification sent to user ${userId}`);
106
  } catch (error) {
107
- this.logger.error(`Error sending push notification to user ${userId}:`, error);
 
 
 
108
  }
109
  }
110
 
@@ -115,11 +125,9 @@ export class NotificationsService {
115
  @Cron(CronExpression.EVERY_MINUTE)
116
  async checkDailyReminders() {
117
  const now = new Date();
118
- const currentHHmm = now.toLocaleTimeString("en-GB", {
119
- hour: "2-digit",
120
- minute: "2-digit",
121
- hour12: false,
122
- });
123
 
124
  const today = new Date();
125
  today.setHours(0, 0, 0, 0);
@@ -166,10 +174,13 @@ export class NotificationsService {
166
  user.id,
167
  template.subject,
168
  template.body,
169
- { type: "DAILY_REMINDER" }
170
  );
171
  } catch (error) {
172
- this.logger.error(`Failed to send daily reminder to user ${user.id}:`, error);
 
 
 
173
  }
174
  }
175
  }
@@ -214,7 +225,7 @@ export class NotificationsService {
214
  user.id,
215
  template.subject,
216
  template.body,
217
- { type: "INACTIVITY_REMINDER" }
218
  );
219
 
220
  // Send Email
@@ -224,21 +235,26 @@ export class NotificationsService {
224
  template.body,
225
  "Log BP Reading",
226
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
227
- "#2DE474"
228
  );
229
 
230
  await this.emailService.sendEmail(
231
  user.email,
232
  template.subject,
233
  template.body,
234
- html
235
  );
236
  }
237
  } catch (error) {
238
- this.logger.error(`Failed to process inactivity reminder for user ${user.id}:`, error);
 
 
 
239
  }
240
  }
241
- this.logger.log(`Inactivity check finished. ${users.length} users notified.`);
 
 
242
  }
243
 
244
  /**
@@ -260,10 +276,7 @@ export class NotificationsService {
260
  bloodPressureReadings: {
261
  some: {
262
  recordedAt: { gte: fiveHoursAgo, lte: fourHoursAgo },
263
- OR: [
264
- { systolic: { gte: 130 } },
265
- { diastolic: { gte: 80 } },
266
- ],
267
  },
268
  none: {
269
  recordedAt: { gt: fourHoursAgo },
@@ -290,7 +303,7 @@ export class NotificationsService {
290
  user.id,
291
  template.subject,
292
  template.body,
293
- { type: "FOLLOW_UP" }
294
  );
295
 
296
  // Send Email
@@ -300,18 +313,21 @@ export class NotificationsService {
300
  template.body,
301
  "Recheck BP Now",
302
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
303
- "#FF9B3E"
304
  );
305
 
306
  await this.emailService.sendEmail(
307
  user.email,
308
  template.subject,
309
  template.body,
310
- html
311
  );
312
  }
313
  } catch (error) {
314
- this.logger.error(`Failed to process follow-up reminder for user ${user.id}:`, error);
 
 
 
315
  }
316
  }
317
  }
@@ -346,34 +362,61 @@ export class NotificationsService {
346
  // 1. Creeping Rise Detection (Consistent rise over 3+ readings)
347
  let riseCount = 0;
348
  for (let i = 1; i < readings.length; i++) {
349
- if (readings[i].systolic > readings[i-1].systolic) riseCount++;
350
  else riseCount = 0;
351
  }
352
  if (riseCount >= 3) {
353
- await this.sendTrendAlert(user.id, TREND_ALERT_TEMPLATES.CREEPING_RISE, "CREEPING_RISE", user.email);
 
 
 
 
 
354
  continue; // Don't spam multiple trend alerts
355
  }
356
 
357
  // 2. Repeated High Readings (3+ elevated readings in a week)
358
- const highReadings = readings.filter(r => r.systolic >= 135 || r.diastolic >= 85);
 
 
359
  if (highReadings.length >= 3) {
360
- await this.sendTrendAlert(user.id, TREND_ALERT_TEMPLATES.REPEATED_HIGH, "REPEATED_HIGH", user.email);
 
 
 
 
 
361
  continue;
362
  }
363
 
364
  // 3. Sudden Spike Detection (Latest reading is 20% > personal average)
365
- const avgSystolic = readings.slice(0, -1).reduce((acc, curr) => acc + curr.systolic, 0) / (readings.length - 1);
 
 
366
  const latest = readings[readings.length - 1];
367
  if (latest.systolic > avgSystolic * 1.2) {
368
- await this.sendTrendAlert(user.id, TREND_ALERT_TEMPLATES.SUDDEN_SPIKE, "SUDDEN_SPIKE", user.email);
 
 
 
 
 
369
  }
370
  } catch (error) {
371
- this.logger.error(`Failed to analyze trends for user ${user.id}:`, error);
 
 
 
372
  }
373
  }
374
  }
375
 
376
- private async sendTrendAlert(userId: string, template: any, trendType: string, email?: string | null) {
 
 
 
 
 
377
  await this.prisma.notification.create({
378
  data: {
379
  userId,
@@ -383,12 +426,10 @@ export class NotificationsService {
383
  },
384
  });
385
 
386
- await this.sendPushNotification(
387
- userId,
388
- template.subject,
389
- template.body,
390
- { type: "TREND_ALERT", trendType }
391
- );
392
 
393
  if (email) {
394
  const html = generateEmailHtml(
@@ -396,14 +437,14 @@ export class NotificationsService {
396
  template.body,
397
  "View Trends",
398
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
399
- "#FF9B3E"
400
  );
401
 
402
  await this.emailService.sendEmail(
403
  email,
404
  template.subject,
405
  template.body,
406
- html
407
  );
408
  }
409
  }
@@ -413,14 +454,14 @@ export class NotificationsService {
413
  */
414
  async sendCarePriorityNotification(
415
  userId: string,
416
- priority: CarePriority
417
  ): Promise<void> {
418
  const template = CARE_PRIORITY_TEMPLATES[priority];
419
  const email = await this.getUserEmail(userId);
420
 
421
  // Log notification
422
  this.logger.log(`[NOTIFICATION] User: ${userId}, Priority: ${priority}`);
423
-
424
  // Persist to database
425
  await this.prisma.notification.create({
426
  data: {
@@ -438,24 +479,26 @@ export class NotificationsService {
438
  `${template.body}\n\n${template.callToAction}`,
439
  "View Details",
440
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
441
- priority === CarePriority.EMERGENCY ? "#FF4B4B" : priority === CarePriority.URGENT_REVIEW ? "#FF9B3E" : "#2DE474"
 
 
 
 
442
  );
443
 
444
  await this.emailService.sendEmail(
445
  email,
446
  template.subject,
447
  `${template.body}\n\n${template.callToAction}`,
448
- html
449
  );
450
  }
451
 
452
  // Send Push
453
- await this.sendPushNotification(
454
- userId,
455
- template.subject,
456
- template.body,
457
- { type: "ESCALATION_ALERT", priority }
458
- );
459
 
460
  this.logger.log(`✅ Notification handled for user ${userId}`);
461
  }
@@ -466,13 +509,13 @@ export class NotificationsService {
466
  async sendSevereBPAlert(
467
  userId: string,
468
  systolic: number,
469
- diastolic: number
470
  ): Promise<void> {
471
  const template = BP_ALERT_TEMPLATES.SEVERE_HYPERTENSION;
472
  const email = await this.getUserEmail(userId);
473
 
474
  this.logger.warn(
475
- `[ALERT] Severe BP for user ${userId}: ${systolic}/${diastolic}`
476
  );
477
 
478
  const message = `${template.body}\nReading: ${systolic}/${diastolic}\n\n${template.callToAction}`;
@@ -493,24 +536,18 @@ export class NotificationsService {
493
  message,
494
  "Log Now",
495
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
496
- "#FF4B4B"
497
  );
498
 
499
- await this.emailService.sendEmail(
500
- email,
501
- template.subject,
502
- message,
503
- html
504
- );
505
  }
506
 
507
  // Send Push
508
- await this.sendPushNotification(
509
- userId,
510
- template.subject,
511
- template.body,
512
- { type: "TREND_ALERT", systolic, diastolic }
513
- );
514
 
515
  this.logger.log(`✅ Severe BP alert handled for user ${userId}`);
516
  }
@@ -521,13 +558,13 @@ export class NotificationsService {
521
  async sendElevatedBPNotification(
522
  userId: string,
523
  systolic: number,
524
- diastolic: number
525
  ): Promise<void> {
526
  const template = BP_ALERT_TEMPLATES.ELEVATED_BP;
527
  const email = await this.getUserEmail(userId);
528
 
529
  this.logger.log(
530
- `[NOTIFICATION] Elevated BP for user ${userId}: ${systolic}/${diastolic}`
531
  );
532
 
533
  const message = `${template.body}\nReading: ${systolic}/${diastolic}\n\n${template.callToAction}`;
@@ -548,24 +585,18 @@ export class NotificationsService {
548
  message,
549
  "Log Now",
550
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
551
- "#FF9B3E"
552
  );
553
 
554
- await this.emailService.sendEmail(
555
- email,
556
- template.subject,
557
- message,
558
- html
559
- );
560
  }
561
 
562
  // Send Push
563
- await this.sendPushNotification(
564
- userId,
565
- template.subject,
566
- template.body,
567
- { type: "TREND_ALERT", systolic, diastolic }
568
- );
569
 
570
  this.logger.log(`✅ Elevated BP notification handled for user ${userId}`);
571
  }
@@ -575,13 +606,13 @@ export class NotificationsService {
575
  */
576
  async sendDangerousSymptomAlert(
577
  userId: string,
578
- symptoms: string[]
579
  ): Promise<void> {
580
  const template = SYMPTOM_ALERT_TEMPLATES.DANGEROUS_COMBINATION;
581
  const email = await this.getUserEmail(userId);
582
 
583
  this.logger.warn(
584
- `[ALERT] Dangerous symptoms for user ${userId}: ${symptoms.join(", ")}`
585
  );
586
 
587
  const message = `${template.body}\nSymptoms reported: ${symptoms.join(", ")}\n\n${template.callToAction}`;
@@ -602,24 +633,17 @@ export class NotificationsService {
602
  message,
603
  "Open App",
604
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
605
- "#FF4B4B"
606
  );
607
 
608
- await this.emailService.sendEmail(
609
- email,
610
- template.subject,
611
- message,
612
- html
613
- );
614
  }
615
 
616
  // Send Push
617
- await this.sendPushNotification(
618
- userId,
619
- template.subject,
620
- template.body,
621
- { type: "TREND_ALERT", symptoms }
622
- );
623
 
624
  this.logger.log(`✅ Dangerous symptom alert handled for user ${userId}`);
625
  }
@@ -629,13 +653,13 @@ export class NotificationsService {
629
  */
630
  async sendWarningSymptomNotification(
631
  userId: string,
632
- symptom: string
633
  ): Promise<void> {
634
  const template = SYMPTOM_ALERT_TEMPLATES.SINGLE_WARNING_SYMPTOM;
635
  const email = await this.getUserEmail(userId);
636
 
637
  this.logger.log(
638
- `[NOTIFICATION] Warning symptom for user ${userId}: ${symptom}`
639
  );
640
 
641
  const message = `${template.body}\nSymptom reported: ${symptom}\n\n${template.callToAction}`;
@@ -656,26 +680,21 @@ export class NotificationsService {
656
  message,
657
  "Open App",
658
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
659
- "#FF9B3E"
660
  );
661
 
662
- await this.emailService.sendEmail(
663
- email,
664
- template.subject,
665
- message,
666
- html
667
- );
668
  }
669
 
670
  // Send Push
671
- await this.sendPushNotification(
672
- userId,
673
- template.subject,
674
- template.body,
675
- { type: "TREND_ALERT", symptom }
676
- );
677
 
678
- this.logger.log(`✅ Warning symptom notification handled for user ${userId}`);
 
 
679
  }
680
 
681
  /**
@@ -683,12 +702,14 @@ export class NotificationsService {
683
  */
684
  async sendResetPasswordNotification(
685
  userId: string,
686
- token: string
687
  ): Promise<void> {
688
  const template = AUTH_TEMPLATES.RESET_PASSWORD;
689
  const email = await this.getUserEmail(userId);
690
 
691
- this.logger.log(`[AUTH] Reset password request for user ${userId}. Email: ${email || 'N/A'}`);
 
 
692
 
693
  const resetLink = `${this.configService.get("FRONTEND_URL")}/reset-password?token=${token}`;
694
  const message = `${template.body}\n\nReset Link: ${resetLink}`;
@@ -710,15 +731,10 @@ export class NotificationsService {
710
  template.body,
711
  "Reset Password",
712
  resetLink,
713
- "#1E6BFF" // Use setup blue for auth actions
714
  );
715
 
716
- await this.emailService.sendEmail(
717
- email,
718
- template.subject,
719
- message,
720
- html
721
- );
722
  }
723
 
724
  // Send Push
@@ -726,10 +742,12 @@ export class NotificationsService {
726
  userId,
727
  template.subject,
728
  template.body,
729
- { type: "SESSION_EXPIRY" } // Using session expiry as a proxy for generic auth actions
730
  );
731
 
732
- this.logger.log(`✅ Reset password notification handled for user ${userId}`);
 
 
733
  }
734
 
735
  /**
 
36
  constructor(
37
  private readonly prisma: PrismaService,
38
  private readonly emailService: EmailService,
39
+ private readonly configService: ConfigService,
40
  ) {}
41
 
42
  /**
 
68
  userId: string,
69
  title: string,
70
  body: string,
71
+ data?: any,
72
  ): Promise<void> {
73
  const user = await this.prisma.userAuth.findUnique({
74
  where: { id: userId },
 
76
  });
77
 
78
  if (!user?.pushToken) {
79
+ this.logger.debug(
80
+ `No push token found for user ${userId}, skipping push.`,
81
+ );
82
  return;
83
  }
84
 
85
  const { Expo } = await import("expo-server-sdk");
86
  if (!Expo.isExpoPushToken(user.pushToken)) {
87
+ this.logger.error(
88
+ `Push token ${user.pushToken} is not a valid Expo push token`,
89
+ );
90
  return;
91
  }
92
 
 
104
  const expo = await this.getExpo();
105
  const chunks = expo.chunkPushNotifications(messages);
106
  for (const chunk of chunks) {
107
+ const tickets = await expo.sendPushNotificationsAsync(chunk);
108
+ this.logger.log(
109
+ `Push notification tickets for user ${userId}: ${JSON.stringify(tickets)}`,
110
+ );
111
  }
112
  this.logger.log(`Push notification sent to user ${userId}`);
113
  } catch (error) {
114
+ this.logger.error(
115
+ `Error sending push notification to user ${userId}:`,
116
+ error,
117
+ );
118
  }
119
  }
120
 
 
125
  @Cron(CronExpression.EVERY_MINUTE)
126
  async checkDailyReminders() {
127
  const now = new Date();
128
+ const hours = now.getHours().toString().padStart(2, "0");
129
+ const minutes = now.getMinutes().toString().padStart(2, "0");
130
+ const currentHHmm = `${hours}:${minutes}`;
 
 
131
 
132
  const today = new Date();
133
  today.setHours(0, 0, 0, 0);
 
174
  user.id,
175
  template.subject,
176
  template.body,
177
+ { type: "BP_REMINDER" },
178
  );
179
  } catch (error) {
180
+ this.logger.error(
181
+ `Failed to send daily reminder to user ${user.id}:`,
182
+ error,
183
+ );
184
  }
185
  }
186
  }
 
225
  user.id,
226
  template.subject,
227
  template.body,
228
+ { type: "INACTIVITY_REMINDER" },
229
  );
230
 
231
  // Send Email
 
235
  template.body,
236
  "Log BP Reading",
237
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
238
+ "#2DE474",
239
  );
240
 
241
  await this.emailService.sendEmail(
242
  user.email,
243
  template.subject,
244
  template.body,
245
+ html,
246
  );
247
  }
248
  } catch (error) {
249
+ this.logger.error(
250
+ `Failed to process inactivity reminder for user ${user.id}:`,
251
+ error,
252
+ );
253
  }
254
  }
255
+ this.logger.log(
256
+ `Inactivity check finished. ${users.length} users notified.`,
257
+ );
258
  }
259
 
260
  /**
 
276
  bloodPressureReadings: {
277
  some: {
278
  recordedAt: { gte: fiveHoursAgo, lte: fourHoursAgo },
279
+ OR: [{ systolic: { gte: 130 } }, { diastolic: { gte: 80 } }],
 
 
 
280
  },
281
  none: {
282
  recordedAt: { gt: fourHoursAgo },
 
303
  user.id,
304
  template.subject,
305
  template.body,
306
+ { type: "FOLLOW_UP" },
307
  );
308
 
309
  // Send Email
 
313
  template.body,
314
  "Recheck BP Now",
315
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
316
+ "#FF9B3E",
317
  );
318
 
319
  await this.emailService.sendEmail(
320
  user.email,
321
  template.subject,
322
  template.body,
323
+ html,
324
  );
325
  }
326
  } catch (error) {
327
+ this.logger.error(
328
+ `Failed to process follow-up reminder for user ${user.id}:`,
329
+ error,
330
+ );
331
  }
332
  }
333
  }
 
362
  // 1. Creeping Rise Detection (Consistent rise over 3+ readings)
363
  let riseCount = 0;
364
  for (let i = 1; i < readings.length; i++) {
365
+ if (readings[i].systolic > readings[i - 1].systolic) riseCount++;
366
  else riseCount = 0;
367
  }
368
  if (riseCount >= 3) {
369
+ await this.sendTrendAlert(
370
+ user.id,
371
+ TREND_ALERT_TEMPLATES.CREEPING_RISE,
372
+ "CREEPING_RISE",
373
+ user.email,
374
+ );
375
  continue; // Don't spam multiple trend alerts
376
  }
377
 
378
  // 2. Repeated High Readings (3+ elevated readings in a week)
379
+ const highReadings = readings.filter(
380
+ (r) => r.systolic >= 135 || r.diastolic >= 85,
381
+ );
382
  if (highReadings.length >= 3) {
383
+ await this.sendTrendAlert(
384
+ user.id,
385
+ TREND_ALERT_TEMPLATES.REPEATED_HIGH,
386
+ "REPEATED_HIGH",
387
+ user.email,
388
+ );
389
  continue;
390
  }
391
 
392
  // 3. Sudden Spike Detection (Latest reading is 20% > personal average)
393
+ const avgSystolic =
394
+ readings.slice(0, -1).reduce((acc, curr) => acc + curr.systolic, 0) /
395
+ (readings.length - 1);
396
  const latest = readings[readings.length - 1];
397
  if (latest.systolic > avgSystolic * 1.2) {
398
+ await this.sendTrendAlert(
399
+ user.id,
400
+ TREND_ALERT_TEMPLATES.SUDDEN_SPIKE,
401
+ "SUDDEN_SPIKE",
402
+ user.email,
403
+ );
404
  }
405
  } catch (error) {
406
+ this.logger.error(
407
+ `Failed to analyze trends for user ${user.id}:`,
408
+ error,
409
+ );
410
  }
411
  }
412
  }
413
 
414
+ private async sendTrendAlert(
415
+ userId: string,
416
+ template: any,
417
+ trendType: string,
418
+ email?: string | null,
419
+ ) {
420
  await this.prisma.notification.create({
421
  data: {
422
  userId,
 
426
  },
427
  });
428
 
429
+ await this.sendPushNotification(userId, template.subject, template.body, {
430
+ type: "TREND_ALERT",
431
+ trendType,
432
+ });
 
 
433
 
434
  if (email) {
435
  const html = generateEmailHtml(
 
437
  template.body,
438
  "View Trends",
439
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
440
+ "#FF9B3E",
441
  );
442
 
443
  await this.emailService.sendEmail(
444
  email,
445
  template.subject,
446
  template.body,
447
+ html,
448
  );
449
  }
450
  }
 
454
  */
455
  async sendCarePriorityNotification(
456
  userId: string,
457
+ priority: CarePriority,
458
  ): Promise<void> {
459
  const template = CARE_PRIORITY_TEMPLATES[priority];
460
  const email = await this.getUserEmail(userId);
461
 
462
  // Log notification
463
  this.logger.log(`[NOTIFICATION] User: ${userId}, Priority: ${priority}`);
464
+
465
  // Persist to database
466
  await this.prisma.notification.create({
467
  data: {
 
479
  `${template.body}\n\n${template.callToAction}`,
480
  "View Details",
481
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
482
+ priority === CarePriority.EMERGENCY
483
+ ? "#FF4B4B"
484
+ : priority === CarePriority.URGENT_REVIEW
485
+ ? "#FF9B3E"
486
+ : "#2DE474",
487
  );
488
 
489
  await this.emailService.sendEmail(
490
  email,
491
  template.subject,
492
  `${template.body}\n\n${template.callToAction}`,
493
+ html,
494
  );
495
  }
496
 
497
  // Send Push
498
+ await this.sendPushNotification(userId, template.subject, template.body, {
499
+ type: "ESCALATION_ALERT",
500
+ priority,
501
+ });
 
 
502
 
503
  this.logger.log(`✅ Notification handled for user ${userId}`);
504
  }
 
509
  async sendSevereBPAlert(
510
  userId: string,
511
  systolic: number,
512
+ diastolic: number,
513
  ): Promise<void> {
514
  const template = BP_ALERT_TEMPLATES.SEVERE_HYPERTENSION;
515
  const email = await this.getUserEmail(userId);
516
 
517
  this.logger.warn(
518
+ `[ALERT] Severe BP for user ${userId}: ${systolic}/${diastolic}`,
519
  );
520
 
521
  const message = `${template.body}\nReading: ${systolic}/${diastolic}\n\n${template.callToAction}`;
 
536
  message,
537
  "Log Now",
538
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
539
+ "#FF4B4B",
540
  );
541
 
542
+ await this.emailService.sendEmail(email, template.subject, message, html);
 
 
 
 
 
543
  }
544
 
545
  // Send Push
546
+ await this.sendPushNotification(userId, template.subject, template.body, {
547
+ type: "TREND_ALERT",
548
+ systolic,
549
+ diastolic,
550
+ });
 
551
 
552
  this.logger.log(`✅ Severe BP alert handled for user ${userId}`);
553
  }
 
558
  async sendElevatedBPNotification(
559
  userId: string,
560
  systolic: number,
561
+ diastolic: number,
562
  ): Promise<void> {
563
  const template = BP_ALERT_TEMPLATES.ELEVATED_BP;
564
  const email = await this.getUserEmail(userId);
565
 
566
  this.logger.log(
567
+ `[NOTIFICATION] Elevated BP for user ${userId}: ${systolic}/${diastolic}`,
568
  );
569
 
570
  const message = `${template.body}\nReading: ${systolic}/${diastolic}\n\n${template.callToAction}`;
 
585
  message,
586
  "Log Now",
587
  `${this.configService.get("FRONTEND_URL")}/bp-entry`,
588
+ "#FF9B3E",
589
  );
590
 
591
+ await this.emailService.sendEmail(email, template.subject, message, html);
 
 
 
 
 
592
  }
593
 
594
  // Send Push
595
+ await this.sendPushNotification(userId, template.subject, template.body, {
596
+ type: "TREND_ALERT",
597
+ systolic,
598
+ diastolic,
599
+ });
 
600
 
601
  this.logger.log(`✅ Elevated BP notification handled for user ${userId}`);
602
  }
 
606
  */
607
  async sendDangerousSymptomAlert(
608
  userId: string,
609
+ symptoms: string[],
610
  ): Promise<void> {
611
  const template = SYMPTOM_ALERT_TEMPLATES.DANGEROUS_COMBINATION;
612
  const email = await this.getUserEmail(userId);
613
 
614
  this.logger.warn(
615
+ `[ALERT] Dangerous symptoms for user ${userId}: ${symptoms.join(", ")}`,
616
  );
617
 
618
  const message = `${template.body}\nSymptoms reported: ${symptoms.join(", ")}\n\n${template.callToAction}`;
 
633
  message,
634
  "Open App",
635
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
636
+ "#FF4B4B",
637
  );
638
 
639
+ await this.emailService.sendEmail(email, template.subject, message, html);
 
 
 
 
 
640
  }
641
 
642
  // Send Push
643
+ await this.sendPushNotification(userId, template.subject, template.body, {
644
+ type: "TREND_ALERT",
645
+ symptoms,
646
+ });
 
 
647
 
648
  this.logger.log(`✅ Dangerous symptom alert handled for user ${userId}`);
649
  }
 
653
  */
654
  async sendWarningSymptomNotification(
655
  userId: string,
656
+ symptom: string,
657
  ): Promise<void> {
658
  const template = SYMPTOM_ALERT_TEMPLATES.SINGLE_WARNING_SYMPTOM;
659
  const email = await this.getUserEmail(userId);
660
 
661
  this.logger.log(
662
+ `[NOTIFICATION] Warning symptom for user ${userId}: ${symptom}`,
663
  );
664
 
665
  const message = `${template.body}\nSymptom reported: ${symptom}\n\n${template.callToAction}`;
 
680
  message,
681
  "Open App",
682
  `${this.configService.get("FRONTEND_URL")}/(tabs)/tracking`,
683
+ "#FF9B3E",
684
  );
685
 
686
+ await this.emailService.sendEmail(email, template.subject, message, html);
 
 
 
 
 
687
  }
688
 
689
  // Send Push
690
+ await this.sendPushNotification(userId, template.subject, template.body, {
691
+ type: "TREND_ALERT",
692
+ symptom,
693
+ });
 
 
694
 
695
+ this.logger.log(
696
+ `✅ Warning symptom notification handled for user ${userId}`,
697
+ );
698
  }
699
 
700
  /**
 
702
  */
703
  async sendResetPasswordNotification(
704
  userId: string,
705
+ token: string,
706
  ): Promise<void> {
707
  const template = AUTH_TEMPLATES.RESET_PASSWORD;
708
  const email = await this.getUserEmail(userId);
709
 
710
+ this.logger.log(
711
+ `[AUTH] Reset password request for user ${userId}. Email: ${email || "N/A"}`,
712
+ );
713
 
714
  const resetLink = `${this.configService.get("FRONTEND_URL")}/reset-password?token=${token}`;
715
  const message = `${template.body}\n\nReset Link: ${resetLink}`;
 
731
  template.body,
732
  "Reset Password",
733
  resetLink,
734
+ "#1E6BFF", // Use setup blue for auth actions
735
  );
736
 
737
+ await this.emailService.sendEmail(email, template.subject, message, html);
 
 
 
 
 
738
  }
739
 
740
  // Send Push
 
742
  userId,
743
  template.subject,
744
  template.body,
745
+ { type: "SESSION_EXPIRY" }, // Using session expiry as a proxy for generic auth actions
746
  );
747
 
748
+ this.logger.log(
749
+ `✅ Reset password notification handled for user ${userId}`,
750
+ );
751
  }
752
 
753
  /**