samoulla-backend / controllers /notificationController.js
Samoulla Sync Bot
Auto-deploy Samoulla Backend: b68e45770de26ed39feb4b1c0925e5345eb3a61d
634b9bb
const Notification = require('../models/notificationModel');
const Newsletter = require('../models/newsletterModel');
const { getIO } = require('../utils/socket');
// Helper to emit and get unread count
const getAndEmitUnreadCount = async (userId) => {
try {
const unreadCount = await Notification.countDocuments({
user: userId,
isRead: false,
});
getIO().to(userId.toString()).emit('unreadCountUpdate', unreadCount);
return unreadCount;
} catch (err) {
console.error('Error emitting unread count:', err);
return 0;
}
};
// Get all notifications for the authenticated user
exports.getMyNotifications = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in to view notifications',
});
}
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 20;
const skip = (page - 1) * limit;
// Filter options
const filter = { user: req.user._id };
if (req.query.isRead !== undefined) {
filter.isRead = req.query.isRead === 'true';
}
if (req.query.type) {
filter.type = req.query.type;
}
const notifications = await Notification.find(filter)
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit)
.populate('relatedId');
const total = await Notification.countDocuments(filter);
const unreadCount = await Notification.countDocuments({
user: req.user._id,
isRead: false,
});
res.status(200).json({
status: 'success',
results: notifications.length,
data: {
notifications,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
unreadCount,
},
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Get unread notification count
exports.getUnreadCount = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
const count = await Notification.countDocuments({
user: req.user._id,
isRead: false,
});
res.status(200).json({
status: 'success',
data: { unreadCount: count },
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Mark a notification as read
exports.markAsRead = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
const notification = await Notification.findOneAndUpdate(
{ _id: req.params.id, user: req.user._id },
{ isRead: true, readAt: new Date() },
{ new: true },
);
if (!notification) {
return res.status(404).json({
status: 'fail',
message: 'Notification not found',
});
}
const unreadCount = await getAndEmitUnreadCount(req.user._id);
// Sync admin dashboard: Notify admins that this notification was read
getIO().emit('notificationReadAdmin', {
notificationId: notification._id,
userId: req.user._id,
isRead: true,
});
res.status(200).json({
status: 'success',
data: { notification, unreadCount },
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Mark all notifications as read
exports.markAllAsRead = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
await Notification.updateMany(
{ user: req.user._id, isRead: false },
{ isRead: true, readAt: new Date() },
);
await getAndEmitUnreadCount(req.user._id);
// Sync admin dashboard: Notify admins that ALL user notifications were read
getIO().emit('notificationAllReadAdmin', {
userId: req.user._id,
});
res.status(200).json({
status: 'success',
message: 'All notifications marked as read',
data: { unreadCount: 0 },
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Delete a notification
exports.deleteNotification = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
const notification = await Notification.findOneAndDelete({
_id: req.params.id,
user: req.user._id,
});
if (!notification) {
return res.status(404).json({
status: 'fail',
message: 'Notification not found',
});
}
const unreadCount = await getAndEmitUnreadCount(req.user._id);
// Sync admin dashboard: Notify admins that this notification was deleted
getIO().emit('notificationDeletedAdmin', notification._id);
res.status(200).json({
status: 'success',
data: { unreadCount },
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Delete all read notifications
exports.deleteAllRead = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
await Notification.deleteMany({
user: req.user._id,
isRead: true,
});
// Sync admin dashboard: Notify admins that ALL user read notifications were deleted
getIO().emit('notificationAllDeletedReadAdmin', {
userId: req.user._id,
});
res.status(200).json({
status: 'success',
message: 'All read notifications deleted',
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Admin: Get all notifications
exports.getAllNotifications = async (req, res) => {
try {
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 50;
const skip = (page - 1) * limit;
const notifications = await Notification.find()
.populate('user', 'name email')
.sort({ createdAt: -1 })
.skip(skip)
.limit(limit);
const total = await Notification.countDocuments();
res.status(200).json({
status: 'success',
results: notifications.length,
data: {
notifications,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
},
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Admin: Create notification for specific user(s)
exports.createNotification = async (req, res) => {
try {
const {
userId,
title,
message,
type,
priority,
metadata,
relatedId,
relatedModel,
} = req.body;
if (!userId || !title || !message) {
return res.status(400).json({
status: 'fail',
message: 'Please provide userId, title, and message',
});
}
const {
createNotification: createNotifService,
} = require('../utils/notificationService');
const notification = await createNotifService({
userId,
title,
message,
type: type || 'general',
priority: priority || 'medium',
metadata,
relatedId: relatedId || undefined,
relatedModel: relatedModel || undefined,
});
if (!notification) {
return res.status(200).json({
status: 'success',
message: 'Notification skipped due to user preferences or error',
data: null,
});
}
res.status(201).json({
status: 'success',
data: { notification },
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Admin: Broadcast notification to all users
exports.broadcastNotification = async (req, res) => {
try {
const {
title,
message,
type,
priority,
metadata,
userRole,
relatedId,
relatedModel,
} = req.body;
if (!title || !message) {
return res.status(400).json({
status: 'fail',
message: 'Please provide title and message',
});
}
const User = require('../models/userModel');
let users;
if (userRole === 'subscriber') {
// Find all active newsletter emails
const subscribers = await Newsletter.find({ isActive: true }).select(
'email',
);
const emails = subscribers.map((s) => s.email);
// Find users whose email is in the subscribers list
users = await User.find({ email: { $in: emails } }).select('_id');
} else {
// Filter users by role if specified
const filter = userRole && userRole !== 'all' ? { role: userRole } : {};
users = await User.find(filter).select('_id');
}
const recipients = users.map((u) => u._id);
const { createBulkNotifications } = require('../utils/notificationService');
const results = await createBulkNotifications(recipients, {
title,
message,
type: type || 'general',
priority: priority || 'medium',
metadata,
relatedId: relatedId || undefined,
relatedModel: relatedModel || undefined,
});
res.status(201).json({
status: 'success',
message: `Notification sent to ${results.length} recipients who opted in`,
data: { count: results.length },
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Admin: Delete any notification (no user ownership check)
exports.adminDeleteNotification = async (req, res) => {
try {
const notification = await Notification.findByIdAndDelete(req.params.id);
if (!notification) {
return res.status(404).json({
status: 'fail',
message: 'Notification not found',
});
}
const io = getIO();
// Notify the specific user that their notification was deleted
io.to(notification.user.toString()).emit(
'notificationDeleted',
notification._id,
);
// Update the user's unread count after deletion
await getAndEmitUnreadCount(notification.user);
// Sync other admins
io.emit('notificationDeletedAdmin', notification._id);
res.status(200).json({
status: 'success',
message: 'Notification deleted successfully',
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Get user's notification preferences
exports.getMyPreferences = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
res.status(200).json({
status: 'success',
data: {
preferences: req.user.notificationPreferences,
},
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Update user's notification preferences
exports.updateMyPreferences = async (req, res) => {
try {
if (!req.user) {
return res.status(401).json({
status: 'fail',
message: 'You must be logged in',
});
}
const { preferences } = req.body;
if (!preferences) {
return res.status(400).json({
status: 'fail',
message: 'Please provide preferences to update',
});
}
// Update user document
const User = require('../models/userModel');
const existingUser = await User.findById(req.user._id);
// Guard: Force-enable notifications only for categories the user has permission for.
// Admins always receive everything. Employees only receive what matches their permissions.
if (req.user.role === 'admin') {
if (preferences.orderUpdates) {
preferences.orderUpdates.app = true;
preferences.orderUpdates.email = true;
}
if (preferences.productUpdates) {
preferences.productUpdates.app = true;
preferences.productUpdates.email = true;
}
delete preferences.vendorOrderVisibility;
} else if (req.user.role === 'employee') {
const empPerms = req.user.permissions || [];
// Only force-enable orderUpdates if the employee has manage_orders permission
if (preferences.orderUpdates && empPerms.includes('manage_orders')) {
preferences.orderUpdates.app = true;
preferences.orderUpdates.email = true;
}
// Only force-enable productUpdates if the employee has manage_products permission
if (preferences.productUpdates && empPerms.includes('manage_products')) {
preferences.productUpdates.app = true;
preferences.productUpdates.email = true;
}
delete preferences.vendorOrderVisibility;
}
// Logic to handle disabledAt and blackoutPeriods for vendorOrderVisibility
if (preferences.vendorOrderVisibility) {
const oldPref =
(existingUser.notificationPreferences &&
existingUser.notificationPreferences.vendorOrderVisibility) ||
{};
const wasEnabled = oldPref.app !== false;
const isEnabled = preferences.vendorOrderVisibility.app !== false;
const periods = oldPref.blackoutPeriods || [];
if (wasEnabled && !isEnabled) {
// Just turned off: set disabledAt (start of new blackout)
preferences.vendorOrderVisibility.disabledAt = new Date();
preferences.vendorOrderVisibility.blackoutPeriods = periods;
} else if (!wasEnabled && isEnabled) {
// Just turned on: close the current blackout period and clear disabledAt
if (oldPref.disabledAt) {
periods.push({
start: oldPref.disabledAt,
end: new Date(),
});
}
preferences.vendorOrderVisibility.blackoutPeriods = periods;
preferences.vendorOrderVisibility.disabledAt = null;
} else if (!wasEnabled && !isEnabled) {
// Stayed off: carry over state
preferences.vendorOrderVisibility.disabledAt = oldPref.disabledAt;
preferences.vendorOrderVisibility.blackoutPeriods = periods;
} else {
// Stayed on: carry over state
preferences.vendorOrderVisibility.disabledAt = null;
preferences.vendorOrderVisibility.blackoutPeriods = periods;
}
}
const user = await User.findByIdAndUpdate(
req.user._id,
{ notificationPreferences: preferences },
{ new: true, runValidators: true },
);
res.status(200).json({
status: 'success',
data: {
preferences: user.notificationPreferences,
},
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message,
});
}
};
// Admin: Get current vendor preference state (reads first active vendor as representative)
exports.getVendorGlobalPreferences = async (req, res) => {
try {
const User = require('../models/userModel');
const vendor = await User.findOne({ role: 'vendor', isActive: true })
.select('notificationPreferences')
.lean();
const defaults = {
orderUpdates: { app: true, email: true },
productUpdates: { app: true, email: true },
vendorOrderVisibility: { app: true, email: true },
};
if (!vendor || !vendor.notificationPreferences) {
return res
.status(200)
.json({ status: 'success', data: { preferences: defaults } });
}
const p = vendor.notificationPreferences;
return res.status(200).json({
status: 'success',
data: {
preferences: {
orderUpdates: {
app: p.orderUpdates?.app !== false,
email: p.orderUpdates?.email !== false,
},
productUpdates: {
app: p.productUpdates?.app !== false,
email: p.productUpdates?.email !== false,
},
vendorOrderVisibility: {
app: p.vendorOrderVisibility?.app !== false,
email: p.vendorOrderVisibility?.email !== false,
},
},
},
});
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
};
// Admin: Bulk-update preferences for ALL active vendor users
// This lets admins globally control whether vendors receive notifications / see orders
exports.updateAllVendorPreferences = async (req, res) => {
try {
if (!req.user) {
return res
.status(401)
.json({ status: 'fail', message: 'You must be logged in' });
}
const { preferences } = req.body;
if (!preferences) {
return res
.status(400)
.json({ status: 'fail', message: 'Please provide preferences' });
}
const User = require('../models/userModel');
const now = new Date();
// Find all active vendor users
const vendors = await User.find({ role: 'vendor', isActive: true }).select(
'_id notificationPreferences',
);
if (vendors.length === 0) {
return res.status(200).json({
status: 'success',
message: 'No active vendors found',
data: { updatedCount: 0 },
});
}
const bulkOps = vendors.map((vendor) => {
const existingPrefs = vendor.notificationPreferences || {};
const updatedPrefs = { ...existingPrefs };
// Apply orderUpdates preference β€” app and email are always kept in sync
if (preferences.orderUpdates !== undefined) {
const val =
typeof preferences.orderUpdates.app === 'boolean'
? preferences.orderUpdates.app
: true;
updatedPrefs.orderUpdates = {
...(existingPrefs.orderUpdates || {}),
app: val,
email: val,
};
}
// Apply productUpdates preference β€” app and email are always kept in sync
if (preferences.productUpdates !== undefined) {
const val =
typeof preferences.productUpdates.app === 'boolean'
? preferences.productUpdates.app
: true;
updatedPrefs.productUpdates = {
...(existingPrefs.productUpdates || {}),
app: val,
email: val,
};
}
// Apply vendorOrderVisibility preference with blackout tracking
if (preferences.vendorOrderVisibility !== undefined) {
const oldVis = existingPrefs.vendorOrderVisibility || {};
const wasEnabled = oldVis.app !== false;
const isEnabled = preferences.vendorOrderVisibility.app !== false;
const periods = oldVis.blackoutPeriods || [];
let disabledAt = oldVis.disabledAt || null;
if (wasEnabled && !isEnabled) {
// Turning OFF: record when it was disabled
disabledAt = now;
} else if (!wasEnabled && isEnabled) {
// Turning ON: close the current blackout period
if (oldVis.disabledAt) {
periods.push({ start: oldVis.disabledAt, end: now });
}
disabledAt = null;
} else if (!wasEnabled && !isEnabled) {
// Stayed off: keep previous disabledAt
} else {
// Stayed on
disabledAt = null;
}
// vendorOrderVisibility: app and email always in sync
updatedPrefs.vendorOrderVisibility = {
app: isEnabled,
email: isEnabled,
disabledAt,
blackoutPeriods: periods,
};
}
return {
updateOne: {
filter: { _id: vendor._id },
update: { $set: { notificationPreferences: updatedPrefs } },
},
};
});
const result = await User.bulkWrite(bulkOps);
// Emit a socket event so online vendors refresh their preferences
const { getIO } = require('../utils/socket');
try {
const io = getIO();
vendors.forEach((vendor) => {
io.to(vendor._id.toString()).emit('preferencesUpdated', {
preferences: bulkOps.find(
(op) =>
op.updateOne.filter._id.toString() === vendor._id.toString(),
)?.updateOne?.update?.$set?.notificationPreferences,
});
});
} catch (socketErr) {
// Non-critical β€” socket might not be available
console.error(
'Socket emit error in updateAllVendorPreferences:',
socketErr,
);
}
res.status(200).json({
status: 'success',
message: `Updated preferences for ${result.modifiedCount} vendor(s)`,
data: { updatedCount: result.modifiedCount },
});
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
};
// ─── Stock Subscription Handlers ─────────────────────────────────────────────
const StockSubscription = require('../models/stockSubscriptionModel');
// POST /notifications/stock-subscribe/:productId
exports.subscribeToStock = async (req, res) => {
try {
const { productId } = req.params;
const userId = req.user._id;
// Upsert: create if not exists, reset notifiedAt so they get notified again on next restock
await StockSubscription.findOneAndUpdate(
{ user: userId, product: productId },
{ user: userId, product: productId, notifiedAt: null },
{ upsert: true, new: true, setDefaultsOnInsert: true },
);
res.status(200).json({
status: 'success',
data: { subscribed: true },
});
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
};
// DELETE /notifications/stock-subscribe/:productId
exports.unsubscribeFromStock = async (req, res) => {
try {
const { productId } = req.params;
const userId = req.user._id;
await StockSubscription.findOneAndDelete({
user: userId,
product: productId,
});
res.status(200).json({
status: 'success',
data: { subscribed: false },
});
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
};
// GET /notifications/stock-subscribe/:productId
exports.checkStockSubscription = async (req, res) => {
try {
const { productId } = req.params;
const userId = req.user._id;
const sub = await StockSubscription.findOne({
user: userId,
product: productId,
});
res.status(200).json({
status: 'success',
data: { subscribed: !!sub },
});
} catch (err) {
res.status(500).json({ status: 'error', message: err.message });
}
};