/** * Notification Scheduler Service * Manages scheduled AI notifications with predictive timing */ require('dotenv').config(); const { PrismaClient: CMSClient } = require("../../prisma/clients/cms"); const prisma = new CMSClient(); const logger = require('./logger.services'); const { localTime } = require('./time.services'); const { predictOptimalDeliveryTime } = require('./ai.services'); /** * Create a scheduled notification with AI-predicted optimal delivery time * @param {Object} params - Notification parameters * @param {string} params.userID - User ID * @param {Array} params.recentActivities - Recent user activities for pattern analysis * @param {Object} params.notificationContent - The generated notification {title, description} * @param {number} params.analyzedActivityCount - Number of activities analyzed * @param {string} params.activityTypes - Comma-separated activity types * @param {number} params.activityTimeRange - Time range in minutes * @param {string} params.aiModel - AI model used * @param {number} params.processingTime - Time taken to process (ms) * @returns {Promise} Created scheduled notification record */ exports.createScheduledNotification = async (params) => { try { const { userID, recentActivities, notificationContent, analyzedActivityCount, activityTypes, activityTimeRange, aiModel, processingTime } = params; const userToken = await prisma.usersToken.findFirst({ where: { UserID_UT: userID } }); if (!userToken) { throw new Error(`User token not found for userID: ${userID}`); } const historicalActivities = await prisma.usersActivity.findMany({ where: { UUID_UT: userToken.UUID_UT, CreatedAt_UA: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } }, orderBy: { CreatedAt_UA: 'desc' }, take: 50 }); const activityPattern = { userId: userID, currentTime: new Date().toISOString(), recentActivities: recentActivities.map(a => ({ type: a.ActivityType_UA || a.type, createdAt: a.CreatedAt_UA || a.timestamp, params: a.Params_UA || a.params })), historicalPattern: historicalActivities.map(a => ({ type: a.ActivityType_UA, hour: new Date(a.CreatedAt_UA).getHours(), dayOfWeek: new Date(a.CreatedAt_UA).getDay(), createdAt: a.CreatedAt_UA })), stats: { totalActivities: historicalActivities.length, uniqueDays: [...new Set(historicalActivities.map(a => new Date(a.CreatedAt_UA).toISOString().split('T')[0] ))].length, avgActivitiesPerDay: historicalActivities.length / 7 } }; logger.info(`Requesting AI predictive timing for user ${userID}...`); let timingPrediction = await predictOptimalDeliveryTime(activityPattern); if (timingPrediction.error || !timingPrediction.optimalDeliveryTime) { logger.error(`AI timing prediction failed, using fallback`, timingPrediction); const now = new Date(); const fallbackTime = new Date(now.getTime() + 2 * 60 * 60 * 1000); // 2 hours from now timingPrediction = { optimalDeliveryTime: fallbackTime.toISOString(), confidenceScore: 50, reasoning: "AI prediction failed, using default 2-hour delay", userEngagementPattern: "unknown", delayMinutes: 120 }; } const scheduledTime = new Date(timingPrediction.optimalDeliveryTime); const now = new Date(); if (isNaN(scheduledTime.getTime())) { logger.error(`Invalid scheduled time from AI prediction: ${timingPrediction.optimalDeliveryTime}`); const fallbackTime = new Date(now.getTime() + 2 * 60 * 60 * 1000); timingPrediction.optimalDeliveryTime = fallbackTime.toISOString(); timingPrediction.reasoning = "Invalid time corrected to 2-hour delay"; scheduledTime.setTime(fallbackTime.getTime()); } const scheduledNotification = await prisma.aINotification.create({ data: { UserID_AIN: userID, AnalyzedActivities_AIN: analyzedActivityCount, ActivityTypes_AIN: activityTypes, GeneratedTitle_AIN: notificationContent.title, GeneratedDesc_AIN: notificationContent.description, SentStatus_AIN: 'scheduled', ScheduledAt_AIN: localTime(scheduledTime), PredictedConfidence_AIN: timingPrediction.confidenceScore, PredictionReasoning_AIN: timingPrediction.reasoning, UserEngagementPattern_AIN: timingPrediction.userEngagementPattern, DelayMinutes_AIN: timingPrediction.delayMinutes, ActivityTimeRange_AIN: activityTimeRange, AIModel_AIN: aiModel, ProcessingTime_AIN: processingTime, CreatedAt_AIN: localTime(now), UpdatedAt_AIN: localTime(now) } }); logger.info(` Notification scheduled for ${userID} at ${scheduledTime.toISOString()} (${timingPrediction.delayMinutes} min delay, ${timingPrediction.confidenceScore}% confidence)`); logger.info(` Reason: ${timingPrediction.reasoning}`); logger.info(` Pattern: ${timingPrediction.userEngagementPattern}`); return { success: true, notificationID: scheduledNotification.UUID_AIN, scheduledFor: scheduledTime.toISOString(), delayMinutes: timingPrediction.delayMinutes, confidence: timingPrediction.confidenceScore, reasoning: timingPrediction.reasoning, pattern: timingPrediction.userEngagementPattern, notification: { title: notificationContent.title, description: notificationContent.description } }; } catch (error) { logger.error(`Error creating scheduled notification: ${error.message}`, error); throw error; } }; /** * Get all notifications that are due to be sent * @returns {Promise} Array of due notifications */ exports.getDueNotifications = async () => { try { const now = new Date(); const dueNotifications = await prisma.aINotification.findMany({ where: { SentStatus_AIN: 'scheduled', ScheduledAt_AIN: { lte: localTime(now) } }, orderBy: { ScheduledAt_AIN: 'asc' } }); logger.info(`Found ${dueNotifications.length} notifications due for delivery`); return dueNotifications; } catch (error) { logger.error(`Error fetching due notifications: ${error.message}`); throw error; } }; /** * Update notification status after sending * @param {string} notificationID - UUID of the notification * @param {Object} updateData - Update data * @returns {Promise} Updated notification */ exports.updateNotificationStatus = async (notificationID, updateData) => { try { const updated = await prisma.aINotification.update({ where: { UUID_AIN: notificationID }, data: { ...updateData, UpdatedAt_AIN: localTime(new Date()) } }); return updated; } catch (error) { logger.error(`Error updating notification ${notificationID}: ${error.message}`); throw error; } }; /** * Get scheduled notifications statistics * @returns {Promise} Statistics */ exports.getScheduledNotificationsStats = async () => { try { const [scheduled, pending, upcoming] = await Promise.all([ prisma.aINotification.count({ where: { SentStatus_AIN: 'scheduled' } }), prisma.aINotification.count({ where: { SentStatus_AIN: 'scheduled', ScheduledAt_AIN: { lte: localTime(new Date()) } } }), prisma.aINotification.findMany({ where: { SentStatus_AIN: 'scheduled', ScheduledAt_AIN: { gte: localTime(new Date()), lte: localTime(new Date(Date.now() + 24 * 60 * 60 * 1000)) } }, select: { UUID_AIN: true, UserID_AIN: true, ScheduledAt_AIN: true, GeneratedTitle_AIN: true, PredictedConfidence_AIN: true, UserEngagementPattern_AIN: true }, orderBy: { ScheduledAt_AIN: 'asc' }, take: 10 }) ]); return { totalScheduled: scheduled, pendingDelivery: pending, upcomingIn24Hours: upcoming.length, nextScheduled: upcoming }; } catch (error) { logger.error(`Error getting scheduled stats: ${error.message}`); throw error; } }; /** * Cancel a scheduled notification * @param {string} notificationID - UUID of the notification * @returns {Promise} Result */ exports.cancelScheduledNotification = async (notificationID) => { try { const updated = await prisma.aINotification.update({ where: { UUID_AIN: notificationID }, data: { SentStatus_AIN: 'cancelled', UpdatedAt_AIN: localTime(new Date()) } }); logger.info(`Notification ${notificationID} cancelled`); return { success: true, notification: updated }; } catch (error) { logger.error(`Error cancelling notification ${notificationID}: ${error.message}`); throw error; } };