Spaces:
Running
Running
| import { Injectable, Logger } from "@nestjs/common"; | |
| import { Cron, CronExpression } from "@nestjs/schedule"; | |
| import { PrismaService } from "../database/prisma.service"; | |
| import { MonitoringEngineService } from "./monitoring-engine.service"; | |
| import { | |
| FollowUpTaskStatus, | |
| FollowUpTaskType, | |
| MonitoringState, | |
| } from "@prisma/client"; | |
| import { NotificationsService } from "../notifications/notifications.service"; | |
| import { UserProfileService } from "../user-profile/user-profile.service"; | |
| import { AppNotificationType } from "../common/enums/app-notification-type.enum"; | |
| () | |
| export class MonitoringSchedulerService { | |
| private readonly logger = new Logger(MonitoringSchedulerService.name); | |
| constructor( | |
| private readonly prisma: PrismaService, | |
| private readonly monitoringEngineService: MonitoringEngineService, | |
| private readonly notificationsService: NotificationsService, | |
| private readonly userProfileService: UserProfileService, | |
| ) {} | |
| (CronExpression.EVERY_HOUR) | |
| async handleOverdueFollowUps() { | |
| this.logger.log("Checking for overdue follow-up tasks..."); | |
| const now = new Date(); | |
| const overdueTasks = await this.prisma.followUpTask.findMany({ | |
| where: { | |
| dueAt: { lt: now }, | |
| status: FollowUpTaskStatus.PENDING, | |
| }, | |
| }); | |
| for (const task of overdueTasks) { | |
| await this.prisma.followUpTask.update({ | |
| where: { id: task.id }, | |
| data: { status: FollowUpTaskStatus.MISSED }, | |
| }); | |
| this.logger.warn( | |
| `Follow-up task ${task.id} for user ${task.userId} missed.`, | |
| ); | |
| // Trigger escalation (re-evaluate monitoring state) | |
| const result = await this.monitoringEngineService.evaluate(task.userId); | |
| this.logger.log( | |
| `Escalated state for user ${task.userId} due to missed follow-up: ${result.state}`, | |
| ); | |
| // Trigger notification | |
| await this.notificationsService.sendPushNotification( | |
| task.userId, | |
| "Follow-up Missed", | |
| `Your ${task.type} follow-up task was missed. Current state: ${result.state}.`, | |
| { type: AppNotificationType.FOLLOW_UP_MISSED }, | |
| ); | |
| } | |
| this.logger.log( | |
| `Finished checking overdue follow-up tasks. ${overdueTasks.length} tasks updated.`, | |
| ); | |
| } | |
| ("0 9 * * *") // Every day at 9 AM | |
| async handleInactivity() { | |
| this.logger.log("Checking for user inactivity..."); | |
| const now = new Date(); | |
| const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); | |
| const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); | |
| const users = await this.prisma.userAuth.findMany({ | |
| select: { | |
| id: true, | |
| bloodPressureReadings: { | |
| orderBy: { recordedAt: "desc" }, | |
| take: 1, | |
| }, | |
| }, | |
| }); | |
| for (const user of users) { | |
| const lastBpReading = user.bloodPressureReadings[0]; | |
| if (!lastBpReading) { | |
| // No BP ever logged, send initial reminder after 3 days of account creation | |
| try { | |
| const userProfile = await this.userProfileService.findByUserId( | |
| user.id, | |
| ); | |
| if (userProfile && userProfile.createdAt < threeDaysAgo) { | |
| const diffInMs = now.getTime() - userProfile.createdAt.getTime(); | |
| const daysInactive = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); | |
| await this.notificationsService.sendPushNotification( | |
| user.id, | |
| "BP Log Reminder", | |
| `You haven't logged your blood pressure yet in ${daysInactive} days. Please log your first reading.`, | |
| { type: AppNotificationType.INACTIVITY_REMINDER }, | |
| ); | |
| this.logger.log( | |
| `Inactivity reminder sent to user ${user.id} (no BP ever logged, ${daysInactive} days).`, | |
| ); | |
| } | |
| } catch (error) { | |
| // If profile not found, skip for now. Profiling is needed for notifications. | |
| this.logger.debug( | |
| `Skipping inactivity check for user ${user.id}: No profile found.`, | |
| ); | |
| } | |
| continue; | |
| } | |
| const latestReading = await this.prisma.bloodPressureReading.findFirst({ | |
| where: { userId: user.id }, | |
| orderBy: { recordedAt: "desc" }, | |
| select: { recordedAt: true }, | |
| }); | |
| if (!latestReading || latestReading.recordedAt.getTime() !== lastBpReading.recordedAt.getTime()) { | |
| continue; | |
| } | |
| // Calculate days inactive | |
| const diffInMs = now.getTime() - lastBpReading.recordedAt.getTime(); | |
| const daysInactive = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); | |
| if (lastBpReading.recordedAt < sevenDaysAgo) { | |
| // No BP logged in >7 days -> escalate messaging urgency | |
| await this.notificationsService.sendPushNotification( | |
| user.id, | |
| "Urgent BP Reminder", | |
| `It's been over ${daysInactive} days since your last blood pressure reading. Please log it now.`, | |
| { type: AppNotificationType.URGENT_INACTIVITY_REMINDER }, | |
| ); | |
| this.logger.warn( | |
| `Urgent inactivity reminder sent to user ${user.id} (${daysInactive} days).`, | |
| ); | |
| } else if (lastBpReading.recordedAt < threeDaysAgo) { | |
| // No BP logged in >3 days -> send reminder | |
| await this.notificationsService.sendPushNotification( | |
| user.id, | |
| "BP Log Reminder", | |
| `It's been over ${daysInactive} days since your last blood pressure reading. Please log it soon.`, | |
| { type: AppNotificationType.INACTIVITY_REMINDER }, | |
| ); | |
| this.logger.log( | |
| `Inactivity reminder sent to user ${user.id} (${daysInactive} days).`, | |
| ); | |
| } | |
| } | |
| this.logger.log("Finished checking for user inactivity."); | |
| } | |
| } | |