acadflow / deploy-backend /services /notificationService.js
Vijayadhith7's picture
Upload 29 files
57a1132 verified
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
};