const { Project, Notification, User } = require('../../models'); const { Op } = require('sequelize'); const { logError } = require('../../services/logService'); const { shouldSendInAppNotification, shouldSendTelegramNotification, } = require('../../utils/notificationPreferences'); /** * Service to check for due and overdue projects * and create notifications for users */ /** * Check for projects that are due soon or overdue * and create notifications for the project owners */ async function checkDueProjects() { try { const now = new Date(); const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000); const dueProjects = await Project.findAll({ where: { due_date_at: { [Op.not]: null, [Op.lte]: tomorrow, }, status: { [Op.notIn]: ['completed'], }, }, include: [ { model: User, attributes: [ 'id', 'email', 'name', 'notification_preferences', ], }, ], }); if (dueProjects.length === 0) { return { success: true, projectsProcessed: 0, notificationsCreated: 0, }; } let notificationsCreated = 0; for (const project of dueProjects) { try { const dueDate = new Date(project.due_date_at); const isOverdue = dueDate < now; const notificationType = isOverdue ? 'project_overdue' : 'project_due_soon'; const level = isOverdue ? 'error' : 'warning'; // Check if user wants this notification if ( !shouldSendInAppNotification(project.User, notificationType) ) { continue; } // Check for existing notifications const recentNotifications = await Notification.findAll({ where: { user_id: project.user_id, type: { [Op.in]: ['project_due_soon', 'project_overdue'], }, created_at: { [Op.gte]: twoDaysAgo, }, }, }); const existingNotification = recentNotifications.find( (notif) => notif.data?.projectUid === project.uid && notif.type === notificationType ); if (existingNotification) { // If notification was dismissed, don't create it again if (existingNotification.dismissed_at) { continue; } // If notification is unread, delete it before creating the new one // This prevents duplicate notifications from piling up if (!existingNotification.read_at) { await existingNotification.destroy(); } else { // If it was already read, skip creating a new one continue; } } const { title, message } = generateNotificationContent( project.name, dueDate, now, isOverdue ); // Build sources array based on user preferences const sources = []; if ( shouldSendTelegramNotification( project.User, notificationType ) ) { sources.push('telegram'); } await Notification.createNotification({ userId: project.user_id, type: notificationType, title, message, level, sources, data: { projectUid: project.uid, projectName: project.name, dueDate: project.due_date_at, isOverdue, }, sentAt: new Date(), }); notificationsCreated++; } catch (error) { logError( `Error creating notification for project ${project.id}:`, error ); } } return { success: true, projectsProcessed: dueProjects.length, notificationsCreated, }; } catch (error) { logError('Error checking due projects:', error); throw error; } } /** * Generate notification title and message based on project due date */ function generateNotificationContent(projectName, dueDate, now, isOverdue) { if (isOverdue) { const daysOverdue = Math.floor((now - dueDate) / (1000 * 60 * 60 * 24)); const title = 'Project is overdue'; let message; if (daysOverdue === 0) { message = `Your project "${projectName}" was due today`; } else if (daysOverdue === 1) { message = `Your project "${projectName}" was due yesterday`; } else { message = `Your project "${projectName}" was due ${daysOverdue} days ago`; } return { title, message }; } else { const hoursUntilDue = Math.floor((dueDate - now) / (1000 * 60 * 60)); const title = 'Project due soon'; let message; if (hoursUntilDue < 1) { message = `Your project "${projectName}" is due in less than 1 hour`; } else if (hoursUntilDue < 24) { message = `Your project "${projectName}" is due in ${hoursUntilDue} hours`; } else { message = `Your project "${projectName}" is due tomorrow`; } return { title, message }; } } module.exports = { checkDueProjects, };