/** * Notification Scheduler * Handles scheduled and recurring notifications */ import { getNotificationService } from './service'; import { ScheduledNotification, NotificationRecipient, NotificationPayload, NotificationChannel, NotificationPriority } from './types'; export class NotificationScheduler { private static instance: NotificationScheduler; private scheduledNotifications: Map = new Map(); private checkInterval: NodeJS.Timeout | null = null; private isRunning = false; private constructor() {} static getInstance(): NotificationScheduler { if (!NotificationScheduler.instance) { NotificationScheduler.instance = new NotificationScheduler(); } return NotificationScheduler.instance; } /** * Start the scheduler */ start(intervalMs: number = 60000): void { if (this.isRunning) return; this.isRunning = true; console.log('[NotificationScheduler] Started'); // Check immediately this.processScheduledNotifications(); // Set up interval this.checkInterval = setInterval(() => { this.processScheduledNotifications(); }, intervalMs); } /** * Stop the scheduler */ stop(): void { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; } this.isRunning = false; console.log('[NotificationScheduler] Stopped'); } /** * Schedule a notification */ schedule( recipient: NotificationRecipient, payload: NotificationPayload, scheduledFor: Date | string, options?: { channels?: NotificationChannel[]; priority?: NotificationPriority; recurring?: { pattern: 'daily' | 'weekly' | 'monthly'; endDate?: string; }; } ): ScheduledNotification { const id = `sched-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const scheduled: ScheduledNotification = { id, notification: { recipientId: recipient.userId, payload, channels: options?.channels || ['inApp', 'email'], priority: options?.priority || 'medium', retryCount: 0 }, scheduledFor: typeof scheduledFor === 'string' ? scheduledFor : scheduledFor.toISOString(), recurring: options?.recurring, status: 'scheduled' }; this.scheduledNotifications.set(id, scheduled); console.log(`[NotificationScheduler] Scheduled notification ${id} for ${scheduled.scheduledFor}`); return scheduled; } /** * Cancel a scheduled notification */ cancel(id: string): boolean { const scheduled = this.scheduledNotifications.get(id); if (scheduled && scheduled.status === 'scheduled') { scheduled.status = 'cancelled'; return true; } return false; } /** * Get scheduled notification */ getScheduled(id: string): ScheduledNotification | undefined { return this.scheduledNotifications.get(id); } /** * Get all scheduled notifications for a user */ getScheduledForUser(userId: string): ScheduledNotification[] { const result: ScheduledNotification[] = []; this.scheduledNotifications.forEach(scheduled => { if (scheduled.notification.recipientId === userId && scheduled.status === 'scheduled') { result.push(scheduled); } }); return result.sort((a, b) => new Date(a.scheduledFor).getTime() - new Date(b.scheduledFor).getTime() ); } /** * Schedule a plant care reminder */ schedulePlantReminder( userId: string, email: string, plantId: string, plantName: string, reminderType: 'water' | 'fertilize' | 'prune' | 'harvest', scheduledFor: Date ): ScheduledNotification { const reminderMessages = { water: `Time to water your ${plantName}!`, fertilize: `Your ${plantName} needs fertilizing.`, prune: `Consider pruning your ${plantName} for better growth.`, harvest: `Your ${plantName} may be ready for harvest!` }; return this.schedule( { userId, email }, { type: 'plant_reminder', title: `Plant Care Reminder: ${plantName}`, message: reminderMessages[reminderType], data: { plantId, plantName, reminderType }, actionUrl: `/plants/${plantId}` }, scheduledFor, { channels: ['inApp', 'email', 'push'] } ); } /** * Schedule weekly digest */ scheduleWeeklyDigest(userId: string, email: string): ScheduledNotification { // Schedule for next Monday at 9 AM const now = new Date(); const nextMonday = new Date(now); nextMonday.setDate(now.getDate() + ((7 - now.getDay() + 1) % 7 || 7)); nextMonday.setHours(9, 0, 0, 0); return this.schedule( { userId, email }, { type: 'weekly_digest', title: 'Your Weekly LocalGreenChain Summary', message: 'Check out what happened this week!', actionUrl: '/dashboard' }, nextMonday, { channels: ['email'], recurring: { pattern: 'weekly' } } ); } /** * Schedule harvest alert */ scheduleHarvestAlert( userId: string, email: string, batchId: string, cropType: string, estimatedHarvestDate: Date ): ScheduledNotification { // Schedule alert 1 day before harvest const alertDate = new Date(estimatedHarvestDate); alertDate.setDate(alertDate.getDate() - 1); return this.schedule( { userId, email }, { type: 'harvest_ready', title: `Harvest Coming Soon: ${cropType}`, message: `Your ${cropType} batch will be ready for harvest tomorrow!`, data: { batchId, cropType, estimatedHarvestDate: estimatedHarvestDate.toISOString() }, actionUrl: `/vertical-farm/batch/${batchId}` }, alertDate, { channels: ['inApp', 'email', 'push'], priority: 'high' } ); } /** * Process due notifications */ private async processScheduledNotifications(): Promise { const now = new Date(); const notificationService = getNotificationService(); for (const [id, scheduled] of this.scheduledNotifications.entries()) { if (scheduled.status !== 'scheduled') continue; const scheduledTime = new Date(scheduled.scheduledFor); if (scheduledTime <= now) { try { // Send the notification await notificationService.send( { userId: scheduled.notification.recipientId }, scheduled.notification.payload, { channels: scheduled.notification.channels, priority: scheduled.notification.priority } ); // Handle recurring if (scheduled.recurring) { const endDate = scheduled.recurring.endDate ? new Date(scheduled.recurring.endDate) : null; if (!endDate || scheduledTime < endDate) { // Schedule next occurrence const nextDate = this.getNextOccurrence(scheduledTime, scheduled.recurring.pattern); scheduled.scheduledFor = nextDate.toISOString(); console.log(`[NotificationScheduler] Rescheduled ${id} for ${scheduled.scheduledFor}`); } else { scheduled.status = 'sent'; } } else { scheduled.status = 'sent'; } console.log(`[NotificationScheduler] Sent scheduled notification ${id}`); } catch (error: any) { console.error(`[NotificationScheduler] Failed to send ${id}:`, error.message); } } } } /** * Calculate next occurrence date */ private getNextOccurrence(current: Date, pattern: 'daily' | 'weekly' | 'monthly'): Date { const next = new Date(current); switch (pattern) { case 'daily': next.setDate(next.getDate() + 1); break; case 'weekly': next.setDate(next.getDate() + 7); break; case 'monthly': next.setMonth(next.getMonth() + 1); break; } return next; } /** * Get scheduler stats */ getStats(): { isRunning: boolean; total: number; scheduled: number; sent: number; cancelled: number; } { let scheduled = 0; let sent = 0; let cancelled = 0; this.scheduledNotifications.forEach(n => { switch (n.status) { case 'scheduled': scheduled++; break; case 'sent': sent++; break; case 'cancelled': cancelled++; break; } }); return { isRunning: this.isRunning, total: this.scheduledNotifications.size, scheduled, sent, cancelled }; } /** * Clean up old notifications */ cleanup(olderThanDays: number = 30): number { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - olderThanDays); let removed = 0; for (const [id, scheduled] of this.scheduledNotifications.entries()) { if (scheduled.status !== 'scheduled') { const scheduledDate = new Date(scheduled.scheduledFor); if (scheduledDate < cutoff) { this.scheduledNotifications.delete(id); removed++; } } } return removed; } } // Export singleton getter export function getNotificationScheduler(): NotificationScheduler { return NotificationScheduler.getInstance(); }