/*********************************************************************************************************************************************************************** * File Name : notification.controller.js * Type : Controller (Client) * Description : Per-user notification management. * GET /client/notifications — paginated list for the auth user * GET /client/notifications/unseen — unseen count * PATCH /client/notifications/:id/seen — mark one as seen * PATCH /client/notifications/seen-all — mark all as seen * DELETE /client/notifications/clear-all — delete all notifications * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 19, 2026 ***********************************************************************************************************************************************************************/ const UserNotification = require('../../models/notifications/user_notification.mdl'); const mdl_Assets = require('../../models/assets/assets.mdl'); const mediaToken = require('../../services/mediaToken.service'); const R = require('../../utils/response.util'); const { notInFutureOrExpired } = require('../../utils/notificationVisibility.util'); const STICKY_LIMIT = 2; const IMAGE_INCLUDE = { model: mdl_Assets, as: 'image', attributes: ["asset_id", "display_name", "file_url", "thumbnail_url", "storage_provider", "storage_key", "file_type", "mime_type"], required: false, }; // Mirrors controllers/admin/advertisements.controller.js's attachImageStreamToken // — kept duplicated rather than shared across the admin/client boundary. async function attachImageStreamToken(image, req) { if (!image || image.storage_provider !== 's3' || !mediaToken.SUPPORTED_TYPES.includes(image.file_type)) { return image; } const ip = mediaToken.resolveIp(req); const { token } = await mediaToken.issueForAsset(image, req.user?.user_id, ip); image.stream_token = token; image.file_url = null; image.thumbnail_url = null; delete image.storage_key; return image; } // ─── GET /client/notifications ──────────────────────────────────────────────── async function list(req, res) { try { const userId = req.user.user_id; const page = Math.max(1, parseInt(req.query.page) || 1); const limit = Math.min(50, parseInt(req.query.limit) || 20); const offset = (page - 1) * limit; const { count, rows } = await UserNotification.findAndCountAll({ where: { user_id: userId, show_in_notifications: true, ...notInFutureOrExpired() }, order: [['createdAt', 'DESC']], limit, offset, }); return R.success(res, 'Notifications fetched.', { notifications: rows, pagination: { page, limit, total: count, pages: Math.ceil(count / limit) }, }); } catch (err) { console.error('[CLIENT NOTIFICATION] list error:', err); return R.error(res, 'Failed to fetch notifications.'); } } // ─── GET /client/notifications/unseen ──────────────────────────────────────── async function unseenCount(req, res) { if (!req.user) return R.success(res, 'Unseen count fetched.', { count: null }); try { const count = await UserNotification.count({ where: { user_id: req.user.user_id, seen: false, show_in_notifications: true, ...notInFutureOrExpired() }, }); return R.success(res, 'Unseen count fetched.', { count }); } catch (err) { console.error('[CLIENT NOTIFICATION] unseenCount error:', err); return R.error(res, 'Failed to fetch unseen count.'); } } // ─── GET /client/notifications/sticky ───────────────────────────────────── async function stickyAnnouncement(req, res) { try { const rows = await UserNotification.findAll({ where: { user_id: req.user.user_id, seen: false, show_in_sticky: true, type: "announcement", ...notInFutureOrExpired(), }, include: [IMAGE_INCLUDE], order: [["createdAt", "DESC"]], limit: STICKY_LIMIT, }); const notifications = await Promise.all(rows.map(async (row) => { const json = row.toJSON(); if (json.image) await attachImageStreamToken(json.image, req); return json; })); return R.success(res, "Sticky alerts fetched.", { announcements: notifications }); } catch (err) { console.error("[CLIENT NOTIFICATION] stickyAnnouncement error:", err); return R.error(res, "Failed to fetch sticky announcement."); } } // ─── PATCH /client/notifications/:id/seen ──────────────────────────────────── async function markSeen(req, res) { try { const notification = await UserNotification.findOne({ where: { notification_id: req.params.id, user_id: req.user.user_id }, }); if (!notification) return R.error(res, 'Notification not found.', 404); await notification.update({ seen: true, seen_at: new Date() }); return R.success(res, 'Notification marked as seen.', notification); } catch (err) { console.error('[CLIENT NOTIFICATION] markSeen error:', err); return R.error(res, 'Failed to mark notification as seen.'); } } // ─── PATCH /client/notifications/seen-all ──────────────────────────────────── async function markAllSeen(req, res) { try { const now = new Date(); const [count] = await UserNotification.update( { seen: true, seen_at: now }, { where: { user_id: req.user.user_id, seen: false } } ); return R.success(res, `${count} notification(s) marked as seen.`, { count }); } catch (err) { console.error('[CLIENT NOTIFICATION] markAllSeen error:', err); return R.error(res, 'Failed to mark all notifications as seen.'); } } // ─── DELETE /client/notifications/clear-all ────────────────────────────────── async function clearAll(req, res) { try { const count = await UserNotification.destroy({ where: { user_id: req.user.user_id }, }); return R.success(res, `${count} notification(s) cleared.`, { count }); } catch (err) { console.error('[CLIENT NOTIFICATION] clearAll error:', err); return R.error(res, 'Failed to clear notifications.'); } } module.exports = { list, unseenCount, stickyAnnouncement, markSeen, markAllSeen, clearAll };