285 lines
10 KiB
JavaScript
285 lines
10 KiB
JavaScript
/**
|
|
* 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<Object>} 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>} 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<Object>} 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<Object>} 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<Object>} 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;
|
|
}
|
|
};
|