csa-backend-test/app/services/notification-scheduler.serv...

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;
}
};