acadflow / services /notificationService.js
skandaedutech's picture
Upload 14 files
0d3ef02 verified
Raw
History Blame
6.26 kB
const express = require('express');
const router = express.Router();
const { getTenantDb, requireAuth } = require('./auth');
const eventBus = require('./eventBus');
const supabase = require('../db');
// Helper to save notifications in background events
const addNotification = async (notif) => {
const orgId = notif.organization_id || '00000000-0000-0000-0000-000000000001';
const newNotif = {
organization_id: orgId,
branch_id: notif.branch_id || '00000000-0000-0000-0000-000000000002',
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 || ''
};
try {
const { data, error } = await supabase
.from('notifications')
.insert([newNotif])
.select();
if (error) throw error;
console.log(`[NotificationService] Database notification logged: ${notif.title}`);
return data[0];
} catch (err) {
console.error('[NotificationService] Failed to save notification to database:', err);
return null;
}
};
/**
* Centrally registers and logs notifications in PostgreSQL database
*/
const initNotificationService = (io) => {
// ===================================================================
// ๐Ÿ“ฅ EVENT BUS SUBSCRIBERS (Decoupled Automated System Alerts)
// ===================================================================
// 1. Auto Alert for Manual Enrollments
eventBus.subscribe('manual_admission.completed', async (payload) => {
const { student_name, student_id, course, pending_amount, installment_option, organization_id } = payload;
await 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) {
await 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
await 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', async (payload) => {
const { student_name, course, organization_id } = payload;
await 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', async (payload) => {
const { student_name, new_status, organization_id } = payload;
if (new_status === 'Pending') {
await 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, async (req, res) => {
try {
const db = getTenantDb(req);
const { data, error } = await db
.from('active_notifications')
.select('*')
.order('created_at', { ascending: false });
if (error) throw error;
res.json(data || []);
} catch (error) {
console.error('Failed to fetch notifications:', error);
res.status(500).json({ error: 'Failed to fetch notifications' });
}
});
// PUT /api/notifications/:id/read - Mark alert as read
router.put('/:id/read', requireAuth, async (req, res) => {
const { id } = req.params;
try {
const db = getTenantDb(req);
const { error } = await db
.from('notifications')
.update({ is_read: true })
.eq('id', id);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to mark notification read:', 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, async (req, res) => {
const { id } = req.params;
try {
const db = getTenantDb(req);
const { error } = await db
.from('notifications')
.update({ is_resolved: true, is_read: true })
.eq('id', id);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to resolve notification:', 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, async (req, res) => {
try {
const db = getTenantDb(req);
const { error } = await db
.from('notifications')
.update({ is_read: true })
.eq('is_read', false);
if (error) throw error;
res.json({ success: true });
} catch (error) {
console.error('Failed to mark all notifications read:', error);
res.status(500).json({ error: 'Failed to mark notifications read' });
}
});
module.exports = {
router,
initNotificationService
};