2 * @file routes/notifications.js
3 * @module routes/notifications
4 * @description Notification management API for in-app and Slack notifications. Handles user-specific
5 * and global notifications with multi-tenant isolation. Supports Slack webhook integration for
6 * real-time team alerts.
8 * **Notification Types:**
9 * - User-specific (user_id set): Targeted to individual user
10 * - Global (user_id NULL): Visible to all users in tenant
11 * - System-wide (root MSP only): Visible across all tenants
13 * **Common Notification Types:**
14 * - stock: Low stock alerts (product inventory)
15 * - ticket: Ticket updates and assignments
16 * - invoice: Invoice generation and payment alerts
17 * - system: System maintenance and updates
18 * - alert: Critical system alerts
19 * - info: Informational messages
21 * **Multi-Tenant Behavior:**
22 * - Root MSP: Sees all notifications (system + their user-specific)
23 * - Regular tenants: See only notifications for their tenant (global + user-specific)
24 * - User-specific notifications filtered by user_id from JWT
26 * **Slack Integration:**
27 * - Automatic webhook notification when Slack integration active
28 * - Configured per tenant in integrations table
29 * - Non-blocking (failures don't prevent notification creation)
30 * - Message format: Bold title + plain text message
31 * - Optional channel targeting via Slack config
34 * - Low stock alerts (from invoices.js stock management)
35 * - Ticket assignment notifications
36 * - System maintenance announcements
37 * - Invoice generation alerts
38 * - Agent heartbeat failures
39 * - Critical system alerts
42 * - notifications table (notification_id, user_id, customer_id, tenant_id, title, message, type, is_read)
43 * - integrations table (Slack webhook configuration)
44 * - users table (notification recipients)
46 * @requires ../services/db
47 * @requires ../middleware/auth
48 * @requires ../middleware/tenant
49 * @author IBG MSP Development Team
52 * @see {@link module:routes/integrations} for Slack integration configuration
53 * @see {@link module:routes/users} for user management
56const express = require('express');
57const router = express.Router();
58const pool = require('../services/db');
59const authenticateToken = require('../middleware/auth');
61const { getTenantFilter } = require('../middleware/tenant');
64 * @api {get} /notifications List Notifications
65 * @apiName GetNotifications
66 * @apiGroup Notifications
67 * @apiDescription Retrieves notifications for authenticated user. Returns user-specific notifications
68 * plus global notifications visible to entire tenant. Root MSP users see system-wide notifications.
69 * Ordered by creation date (newest first), limited to 100 most recent.
71 * **Notification Visibility:**
72 * - User-specific (user_id = current user): Always visible
73 * - Global (user_id NULL, tenant_id = user's tenant): Visible to all in tenant
74 * - System-wide (user_id NULL, tenant_id NULL): Root MSP only
76 * **Root MSP Behavior:**
77 * Root MSP tenant sees:
78 * - All their personal notifications (user_id = current user)
79 * - All system-wide global notifications (user_id NULL)
81 * **Regular Tenant Behavior:**
82 * Regular tenants see:
83 * - Their personal notifications (user_id = current user)
84 * - Tenant-wide global notifications (user_id NULL AND tenant_id = user's tenant)
86 * **Ordering & Limits:**
87 * - Sorted by created_at DESC (newest first)
88 * - Maximum 100 notifications returned
89 * - Use is_read flag for client-side filtering of unread
90 * @apiHeader {string} Authorization Bearer JWT token
91 * @apiSuccess {object[]} notifications Array of notification objects (max 100)
92 * @apiSuccess {number} notifications.notification_id Unique notification ID
93 * @apiSuccess {number} notifications.user_id Target user ID (NULL for global)
94 * @apiSuccess {number} notifications.customer_id Associated customer ID (if applicable)
95 * @apiSuccess {number} notifications.tenant_id Associated tenant ID (NULL for system-wide)
96 * @apiSuccess {string} notifications.title Notification title/subject
97 * @apiSuccess {string} notifications.message Notification body/details
98 * @apiSuccess {string} notifications.type Notification type (stock, ticket, invoice, system, alert, info)
99 * @apiSuccess {boolean} notifications.is_read Whether user has read notification
100 * @apiSuccess {string} notifications.created_at Creation timestamp
101 * @apiError {number} 500 Server error during notification retrieval
102 * @apiExample {curl} Example Request:
104 * -H "Authorization: Bearer eyJhbGc..." \
105 * "https://api.everydaytech.au/notifications"
106 * @apiExample {json} Success Response:
110 * "notification_id": 42,
112 * "customer_id": null,
114 * "title": "Ticket Assigned",
115 * "message": "Ticket #892 has been assigned to you",
118 * "created_at": "2024-03-11T17:45:00.000Z"
121 * "notification_id": 38,
123 * "customer_id": null,
125 * "title": "Low stock: Dell Latitude 5420 Laptop",
126 * "message": "Stock for product Dell Latitude 5420 Laptop is low (8 < 10)",
129 * "created_at": "2024-03-11T16:30:00.000Z"
133 * @see {@link module:routes/notifications.createNotification} for creating notifications
134 * @see {@link module:routes/notifications.markNotificationRead} for marking as read
136router.get('/', authenticateToken, async (req, res) => {
137 const userId = req.user && req.user.user_id;
138 const tenantId = req.user?.tenantId || req.user?.tenant_id;
139 const isRootTenant = req.tenant?.isMsp || req.user?.is_msp;
141 const { clause: tenantClause, params: tenantParams } = getTenantFilter(req, 'n');
147 // Root tenant sees all notifications (system + user-specific)
148 query = `SELECT * FROM notifications n WHERE (user_id IS NULL OR user_id = $1)`;
151 // Regular tenants only see notifications for their tenant
152 query = `SELECT * FROM notifications n WHERE (user_id = $1 OR (user_id IS NULL AND tenant_id = $2))`;
153 params = [userId, tenantId];
156 if (tenantClause && !isRootTenant) {
157 query += ` AND ${tenantClause}`;
158 params = [...params, ...tenantParams];
161 query += ' ORDER BY created_at DESC LIMIT 100';
162 const result = await pool.query(query, params);
163 res.json(result.rows);
165 console.error('Error fetching notifications:', err);
166 res.status(500).send('Server error');
171 * @api {post} /notifications Create Notification
172 * @apiName CreateNotification
173 * @apiGroup Notifications
174 * @apiDescription Creates a new notification with optional Slack webhook integration. Supports
175 * user-specific and global notifications. Automatically sends to Slack if integration active
178 * **Notification Targeting:**
179 * - User-specific: Set user_id (notification visible only to that user)
180 * - Global: Omit user_id or set NULL (notification visible to all users in tenant)
181 * - Customer-related: Set customer_id to associate with customer record
183 * **Tenant Context:**
184 * - Application determines tenant_id from req.tenant context (JWT)
185 * - Can override with explicit tenant_id parameter (for impersonation)
186 * - Falls back to req.tenant.id if not provided
188 * **Slack Integration:**
189 * If Slack integration configured and active for tenant:
190 * 1. Query integrations table for tenant's Slack config
191 * 2. Extract webhookUrl and optional channel from config JSON
192 * 3. POST to Slack webhook with formatted message
193 * 4. Message format: Bold title + plain text message
194 * 5. Failure non-blocking (notification still created)
196 * **Type Best Practices:**
197 * - stock: Inventory/reorder alerts
198 * - ticket: Ticket assignments, updates, closures
199 * - invoice: Invoice generation, payment, overdue
200 * - system: Maintenance, updates, configuration changes
201 * - alert: Critical issues requiring immediate attention
202 * - info: General informational messages
203 * @apiHeader {string} Authorization Bearer JWT token
204 * @apiHeader {string} Content-Type application/json
205 * @apiParam {number} [user_id] Target user ID (NULL for global notification)
206 * @apiParam {number} [customer_id] Related customer ID (optional)
207 * @apiParam {string} title Notification title/subject (required)
208 * @apiParam {string} message Notification body/details (required)
209 * @apiParam {string} type Notification type (stock, ticket, invoice, system, alert, info)
210 * @apiParam {number} [tenant_id] Override tenant ID (for impersonation, defaults to req.tenant.id)
211 * @apiSuccess {number} notification_id Generated notification ID
212 * @apiSuccess {number} user_id Target user ID
213 * @apiSuccess {number} customer_id Associated customer ID
214 * @apiSuccess {number} tenant_id Associated tenant ID
215 * @apiSuccess {string} title Notification title
216 * @apiSuccess {string} message Notification message
217 * @apiSuccess {string} type Notification type
218 * @apiSuccess {boolean} is_read Always false for new notifications
219 * @apiSuccess {string} created_at Creation timestamp
221 * @apiError {Number} 500 Failed to create notification
223 * @apiExample {curl} Example Request (User-Specific):
225 * -H "Authorization: Bearer eyJhbGc..." \
226 * -H "Content-Type: application/json" \
229 * "title": "Ticket Assigned",
230 * "message": "Ticket #892 has been assigned to you",
233 * "https://api.everydaytech.au/notifications"
235 * @apiExample {curl} Example Request (Global):
237 * -H "Authorization: Bearer eyJhbGc..." \
238 * -H "Content-Type: application/json" \
240 * "title": "System Maintenance",
241 * "message": "Platform maintenance scheduled for March 15, 2024 at 2:00 AM UTC",
244 * "https://api.everydaytech.au/notifications"
246 * @apiExample {json} Success Response:
247 * HTTP/1.1 201 Created
249 * "notification_id": 42,
251 * "customer_id": null,
253 * "title": "Ticket Assigned",
254 * "message": "Ticket #892 has been assigned to you",
257 * "created_at": "2024-03-11T17:45:00.000Z"
261 * @see {@link module:routes/notifications.getNotifications} for retrieving notifications
262 * @see {@link module:routes/integrations} for Slack integration setup
264router.post('/', authenticateToken, async (req, res) => {
265 const { user_id, customer_id, title, message, type, tenant_id } = req.body;
266 let targetTenantId = tenant_id;
267 if (!targetTenantId && req.tenant && req.tenant.id) targetTenantId = req.tenant.id;
269 const result = await pool.query(
270 `INSERT INTO notifications (user_id, customer_id, title, message, type, tenant_id) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
271 [user_id || null, customer_id || null, title, message, type, targetTenantId]
273 // Slack integration: send notification if enabled and configured
275 const slackRes = await pool.query(
276 `SELECT config FROM integrations WHERE tenant_id = $1 AND integration_type = 'slack' AND is_active = true LIMIT 1`,
279 if (slackRes.rows.length) {
280 const slackCfg = slackRes.rows[0].config;
281 const webhookUrl = slackCfg.webhookUrl;
282 const channel = slackCfg.channel || '';
285 text: `*${title}*\n${message}`,
286 channel: channel || undefined
288 await fetch(webhookUrl, {
290 headers: { 'Content-Type': 'application/json' },
291 body: JSON.stringify(payload)
296 console.error('Slack notification error:', e);
298 res.status(201).json(result.rows[0]);
300 console.error('Error creating notification:', err);
301 res.status(500).send('Failed to create notification');
306 * @api {put} /notifications/:id/mark-read Mark Notification as Read
307 * @apiName MarkNotificationRead
308 * @apiGroup Notifications
309 * @apiDescription Marks a notification as read by setting is_read flag to true. Used when user
310 * views notification in UI. Idempotent operation - calling multiple times has same effect.
312 * **UI Integration:**
313 * - Call when user clicks/views notification
314 * - Update notification badge counts in real-time
315 * - Use for "mark all as read" features (call for each notification)
317 * **No Ownership Validation:**
318 * Current implementation does not validate notification belongs to authenticated user.
319 * Consider adding WHERE user_id = $1 clause for security.
320 * @apiHeader {string} Authorization Bearer JWT token
321 * @apiParam {number} id Notification ID (in URL path)
322 * @apiSuccess {number} notification_id Notification ID
323 * @apiSuccess {boolean} is_read Always true after successful update
324 * @apiSuccess {string} updated_at Update timestamp (if column exists)
325 * @apiError {number} 500 Failed to update notification
326 * @apiExample {curl} Example Request:
328 * -H "Authorization: Bearer eyJhbGc..." \
329 * "https://api.everydaytech.au/notifications/42/mark-read"
330 * @apiExample {json} Success Response:
333 * "notification_id": 42,
335 * "customer_id": null,
337 * "title": "Ticket Assigned",
338 * "message": "Ticket #892 has been assigned to you",
341 * "created_at": "2024-03-11T17:45:00.000Z"
344 * @see {@link module:routes/notifications.getNotifications} for retrieving notifications
346router.put('/:id/mark-read', authenticateToken, async (req, res) => {
347 const id = req.params.id;
349 const result = await pool.query('UPDATE notifications SET is_read = TRUE WHERE notification_id = $1 RETURNING *', [id]);
350 res.json(result.rows[0]);
352 console.error('Error marking notification read:', err);
353 res.status(500).send('Failed to update notification');
357module.exports = router;