const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); const { requireAuth } = require('./auth'); const eventBus = require('./eventBus'); // 💾 Local file storage helper (persistent when on HF data mount if available) const NOTIFICATIONS_DIR = fs.existsSync('/data') ? '/data' : path.join(__dirname, '..'); const NOTIFICATIONS_FILE = path.join(NOTIFICATIONS_DIR, 'notifications.json'); // Helper to access notification logs function readNotifications() { try { if (!fs.existsSync(NOTIFICATIONS_FILE)) { return []; } return JSON.parse(fs.readFileSync(NOTIFICATIONS_FILE, 'utf8')); } catch (e) { console.error('Failed to read notifications log:', e); return []; } } function writeNotifications(data) { try { fs.writeFileSync(NOTIFICATIONS_FILE, JSON.stringify(data, null, 2), 'utf8'); } catch (e) { console.error('Failed to write notifications log:', e); } } /** * Centrally registers and logs a notification, then broadcasts it in real-time */ const initNotificationService = (io) => { const addNotification = (notif) => { const notifications = readNotifications(); const newNotif = { id: `notif-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, organization_id: notif.organization_id || '00000000-0000-0000-0000-000000000001', title: notif.title, message: notif.message, type: notif.type || 'SYSTEM_ALERT', priority: notif.priority || 'Low', is_read: false, is_resolved: false, action_url: notif.action_url || '', created_at: new Date().toISOString() }; notifications.unshift(newNotif); writeNotifications(notifications); // 📢 Socket.IO Room Isolation: Emit strictly to clients connected within this specific organization if (io) { io.to(`org-${newNotif.organization_id}`).emit('notification_created', newNotif); } return newNotif; }; // =================================================================== // 📥 EVENT BUS SUBSCRIBERS (Decoupled Automated System Alerts) // =================================================================== // 1. Auto Alert for Manual Enrollments eventBus.subscribe('manual_admission.completed', (payload) => { const { student_name, student_id, course, pending_amount, installment_option, organization_id } = payload; addNotification({ organization_id, title: "New admission completed 🎓", message: `${student_name} enrolled in ${course} (ID: ${student_id}).`, type: "ADMISSION_ALERT", priority: "Medium", action_url: "/admissions" }); if (parseFloat(pending_amount || 0) > 0) { addNotification({ organization_id, title: "Installment generated 💰", message: `Pending balance of ₹${parseFloat(pending_amount).toLocaleString()} for ${student_name} (${installment_option || 'EMI'}).`, type: "PAYMENT_ALERT", priority: "High", action_url: "/admissions" }); } // AI trends trigger alert addNotification({ organization_id, title: `${course} conversion boost 📈`, message: `AI Insight: ${course} enrollments showed a surge after campaign adjustments.`, type: "AI_INSIGHT", priority: "Medium", action_url: "/ai-insights" }); }); // 2. Alert for Payment Completions eventBus.subscribe('payment.completed', (payload) => { const { student_name, course, organization_id } = payload; addNotification({ organization_id, title: "Fee payment completed ✅", message: `Received final fee payment installment from ${student_name} for course: ${course}.`, type: "PAYMENT_ALERT", priority: "Medium", action_url: "/admissions" }); }); // 3. Alert for Real-time counselor lead updates eventBus.subscribe('lead.status_changed', (payload) => { const { student_name, new_status, organization_id } = payload; if (new_status === 'Pending') { addNotification({ organization_id, title: "Follow-up Overdue Alert 🔴", message: `Lead ${student_name} followup has expired. Action required.`, type: "SYSTEM_ALERT", priority: "High", action_url: "/leads" }); } }); return { addNotification }; }; // =================================================================== // REST API Routes // =================================================================== // GET /api/notifications - Fetch all active notifications scoped by active tenant router.get('/', requireAuth, (req, res) => { try { const notifications = readNotifications(); const tenantNotifications = notifications.filter(n => n.organization_id === req.user.organization_id); res.json(tenantNotifications); } catch (error) { res.status(500).json({ error: 'Failed to fetch notification database logs' }); } }); // PUT /api/notifications/:id/read - Mark alert as read router.put('/:id/read', requireAuth, (req, res) => { const { id } = req.params; try { const notifications = readNotifications(); const idx = notifications.findIndex(n => n.id === id && n.organization_id === req.user.organization_id); if (idx !== -1) { notifications[idx].is_read = true; writeNotifications(notifications); return res.json({ success: true }); } res.status(404).json({ error: 'Alert log not found' }); } catch (error) { res.status(500).json({ error: 'Failed to update alert log status' }); } }); // PUT /api/notifications/:id/resolve - Resolve active notification checklist item router.put('/:id/resolve', requireAuth, (req, res) => { const { id } = req.params; try { const notifications = readNotifications(); const idx = notifications.findIndex(n => n.id === id && n.organization_id === req.user.organization_id); if (idx !== -1) { notifications[idx].is_resolved = true; notifications[idx].is_read = true; writeNotifications(notifications); return res.json({ success: true }); } res.status(404).json({ error: 'Alert log not found' }); } catch (error) { res.status(500).json({ error: 'Failed to resolve notification logs' }); } }); // POST /api/notifications/mark-all-read - Clear all unread notifications router.post('/mark-all-read', requireAuth, (req, res) => { try { const notifications = readNotifications(); notifications.forEach(n => { if (n.organization_id === req.user.organization_id) { n.is_read = true; } }); writeNotifications(notifications); res.json({ success: true }); } catch (error) { res.status(500).json({ error: 'Failed to mark notifications read' }); } }); module.exports = { router, initNotificationService };