Spaces:
Running
Running
| 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 | |
| }; | |