bulltrackers-module 1.0.521 → 1.0.522
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,35 +1,124 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Notification Helpers for On-Demand Requests
|
|
3
3
|
* Sends notifications to users when their on-demand sync/computation requests complete
|
|
4
|
+
* UPDATED: Added notification preferences support and task engine notifications
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
|
+
const { readWithMigration, writeWithMigration } = require('../core/path_resolution_helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get user notification preferences
|
|
12
|
+
* @param {object} db - Firestore instance
|
|
13
|
+
* @param {object} collectionRegistry - Collection registry
|
|
14
|
+
* @param {number} userCid - User CID
|
|
15
|
+
* @param {object} config - Config object
|
|
16
|
+
* @returns {Promise<object>} Notification preferences object
|
|
17
|
+
*/
|
|
18
|
+
async function getUserNotificationPreferences(db, collectionRegistry, userCid, config) {
|
|
19
|
+
try {
|
|
20
|
+
// Use readWithMigration to support legacy paths during migration
|
|
21
|
+
const result = await readWithMigration(
|
|
22
|
+
db,
|
|
23
|
+
'signedInUsers',
|
|
24
|
+
'notificationPreferences',
|
|
25
|
+
{ cid: String(userCid) },
|
|
26
|
+
{
|
|
27
|
+
isCollection: false,
|
|
28
|
+
dataType: 'notificationPreferences',
|
|
29
|
+
config,
|
|
30
|
+
documentId: 'settings',
|
|
31
|
+
collectionRegistry
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (result && result.data) {
|
|
36
|
+
const data = result.data;
|
|
37
|
+
// Support both nested structure (settings: {...}) and flat structure
|
|
38
|
+
const preferences = data.settings || data || {};
|
|
39
|
+
|
|
40
|
+
// Default preferences (all enabled except test alerts)
|
|
41
|
+
return {
|
|
42
|
+
syncProcesses: preferences.syncProcesses !== undefined ? preferences.syncProcesses : true,
|
|
43
|
+
userActionCompletions: preferences.userActionCompletions !== undefined ? preferences.userActionCompletions : true,
|
|
44
|
+
watchlistAlerts: preferences.watchlistAlerts !== undefined ? preferences.watchlistAlerts : true,
|
|
45
|
+
testAlerts: preferences.testAlerts !== undefined ? preferences.testAlerts : false
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If document doesn't exist, return defaults
|
|
50
|
+
return {
|
|
51
|
+
syncProcesses: true,
|
|
52
|
+
userActionCompletions: true,
|
|
53
|
+
watchlistAlerts: true,
|
|
54
|
+
testAlerts: false
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// If error, return defaults
|
|
58
|
+
return {
|
|
59
|
+
syncProcesses: true,
|
|
60
|
+
userActionCompletions: true,
|
|
61
|
+
watchlistAlerts: true,
|
|
62
|
+
testAlerts: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if user should receive a notification of a given type
|
|
69
|
+
* @param {object} db - Firestore instance
|
|
70
|
+
* @param {object} collectionRegistry - Collection registry
|
|
71
|
+
* @param {number} userCid - User CID
|
|
72
|
+
* @param {string} notificationType - Notification type ('syncProcesses', 'userActionCompletions', 'watchlistAlerts', 'testAlerts')
|
|
73
|
+
* @param {object} config - Config object
|
|
74
|
+
* @returns {Promise<boolean>} True if user should receive notification
|
|
75
|
+
*/
|
|
76
|
+
async function shouldSendNotification(db, collectionRegistry, userCid, notificationType, config) {
|
|
77
|
+
const preferences = await getUserNotificationPreferences(db, collectionRegistry, userCid, config);
|
|
78
|
+
return preferences[notificationType] === true;
|
|
79
|
+
}
|
|
7
80
|
|
|
8
81
|
/**
|
|
9
82
|
* Send a notification to a user about their on-demand request
|
|
10
|
-
* @param {
|
|
11
|
-
* @param {
|
|
83
|
+
* @param {object} db - Firestore instance
|
|
84
|
+
* @param {object} logger - Logger instance
|
|
12
85
|
* @param {number} userCid - User CID to notify
|
|
13
86
|
* @param {string} type - Notification type ('success', 'error', 'progress')
|
|
14
87
|
* @param {string} title - Notification title
|
|
15
88
|
* @param {string} message - Notification message
|
|
16
|
-
* @param {
|
|
89
|
+
* @param {object} metadata - Additional metadata (requestId, computationName, etc.)
|
|
90
|
+
* @param {object} options - Optional: { collectionRegistry, config, notificationType }
|
|
17
91
|
*/
|
|
18
|
-
async function sendOnDemandNotification(db, logger, userCid, type, title, message, metadata = {}) {
|
|
92
|
+
async function sendOnDemandNotification(db, logger, userCid, type, title, message, metadata = {}, options = {}) {
|
|
93
|
+
// Check notification preferences if collectionRegistry is provided
|
|
94
|
+
if (options.collectionRegistry && options.config && options.notificationType) {
|
|
95
|
+
const shouldSend = await shouldSendNotification(
|
|
96
|
+
db,
|
|
97
|
+
options.collectionRegistry,
|
|
98
|
+
userCid,
|
|
99
|
+
options.notificationType,
|
|
100
|
+
options.config
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!shouldSend) {
|
|
104
|
+
logger.log('INFO', `[sendOnDemandNotification] Skipping notification to user ${userCid} (preference disabled for ${options.notificationType})`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
19
108
|
try {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.doc(notificationId);
|
|
109
|
+
const { collectionRegistry } = options;
|
|
110
|
+
const config = options.config || {};
|
|
111
|
+
|
|
112
|
+
const notificationId = `notif_${Date.now()}_${userCid}_${Math.random().toString(36).substring(2, 9)}`;
|
|
25
113
|
|
|
26
114
|
const notificationData = {
|
|
27
115
|
id: notificationId,
|
|
28
|
-
type: 'on_demand',
|
|
29
|
-
subType: type, // 'success', 'error', 'progress'
|
|
116
|
+
type: metadata.notificationType || 'on_demand',
|
|
117
|
+
subType: type, // 'success', 'error', 'progress', 'info'
|
|
30
118
|
title,
|
|
31
119
|
message,
|
|
32
120
|
read: false,
|
|
121
|
+
timestamp: FieldValue.serverTimestamp(),
|
|
33
122
|
createdAt: FieldValue.serverTimestamp(),
|
|
34
123
|
metadata: {
|
|
35
124
|
...metadata,
|
|
@@ -37,21 +126,30 @@ async function sendOnDemandNotification(db, logger, userCid, type, title, messag
|
|
|
37
126
|
}
|
|
38
127
|
};
|
|
39
128
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
129
|
+
// Write using collection registry
|
|
130
|
+
if (collectionRegistry) {
|
|
131
|
+
await writeWithMigration(
|
|
132
|
+
db,
|
|
133
|
+
'signedInUsers',
|
|
134
|
+
'notifications',
|
|
135
|
+
{ cid: String(userCid), notificationId },
|
|
136
|
+
notificationData,
|
|
137
|
+
{
|
|
138
|
+
isCollection: false,
|
|
139
|
+
merge: false,
|
|
140
|
+
dataType: 'notifications',
|
|
141
|
+
config,
|
|
142
|
+
collectionRegistry
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
// Fallback to legacy path
|
|
147
|
+
const notificationRef = db.collection('user_notifications')
|
|
148
|
+
.doc(String(userCid))
|
|
149
|
+
.collection('notifications')
|
|
150
|
+
.doc(notificationId);
|
|
151
|
+
await notificationRef.set(notificationData);
|
|
152
|
+
}
|
|
55
153
|
|
|
56
154
|
logger.log('INFO', `[sendOnDemandNotification] Sent ${type} notification to user ${userCid}: ${title}`);
|
|
57
155
|
|
|
@@ -64,7 +162,7 @@ async function sendOnDemandNotification(db, logger, userCid, type, title, messag
|
|
|
64
162
|
/**
|
|
65
163
|
* Send notification when task engine completes data fetch
|
|
66
164
|
*/
|
|
67
|
-
async function notifyTaskEngineComplete(db, logger, requestingUserCid, requestId, username, success, error = null) {
|
|
165
|
+
async function notifyTaskEngineComplete(db, logger, requestingUserCid, requestId, username, success, error = null, options = {}) {
|
|
68
166
|
if (!requestingUserCid) return; // No user to notify
|
|
69
167
|
|
|
70
168
|
const type = success ? 'success' : 'error';
|
|
@@ -79,14 +177,83 @@ async function notifyTaskEngineComplete(db, logger, requestingUserCid, requestId
|
|
|
79
177
|
requestId,
|
|
80
178
|
username,
|
|
81
179
|
stage: 'task_engine',
|
|
82
|
-
success
|
|
180
|
+
success,
|
|
181
|
+
notificationType: 'syncProcesses'
|
|
182
|
+
}, {
|
|
183
|
+
...options,
|
|
184
|
+
notificationType: 'syncProcesses'
|
|
83
185
|
});
|
|
84
186
|
}
|
|
85
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Send notification when PI data is refreshed (for users with PI in watchlist)
|
|
190
|
+
* @param {object} db - Firestore instance
|
|
191
|
+
* @param {object} logger - Logger instance
|
|
192
|
+
* @param {object} collectionRegistry - Collection registry
|
|
193
|
+
* @param {number} piCid - Popular Investor CID
|
|
194
|
+
* @param {string} piUsername - Popular Investor username
|
|
195
|
+
* @param {object} config - Config object
|
|
196
|
+
*/
|
|
197
|
+
async function notifyPIDataRefreshed(db, logger, collectionRegistry, piCid, piUsername, config) {
|
|
198
|
+
try {
|
|
199
|
+
// Find all users who have this PI in their watchlist
|
|
200
|
+
// Check both static and dynamic watchlists
|
|
201
|
+
const watchlistMembershipRef = db.collection('WatchlistMembershipData')
|
|
202
|
+
.doc(new Date().toISOString().split('T')[0]);
|
|
203
|
+
|
|
204
|
+
const membershipDoc = await watchlistMembershipRef.get();
|
|
205
|
+
if (!membershipDoc.exists) {
|
|
206
|
+
logger.log('INFO', `[notifyPIDataRefreshed] No watchlist membership data found for today`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const membershipData = membershipDoc.data();
|
|
211
|
+
const piCidStr = String(piCid);
|
|
212
|
+
const piMembership = membershipData[piCidStr];
|
|
213
|
+
|
|
214
|
+
if (!piMembership || !piMembership.users || !Array.isArray(piMembership.users)) {
|
|
215
|
+
logger.log('INFO', `[notifyPIDataRefreshed] No users have PI ${piCid} in their watchlist`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const userIds = piMembership.users.map(u => String(u));
|
|
220
|
+
logger.log('INFO', `[notifyPIDataRefreshed] Found ${userIds.length} users with PI ${piCid} in watchlist`);
|
|
221
|
+
|
|
222
|
+
// Send notification to each user
|
|
223
|
+
const notificationPromises = userIds.map(async (userId) => {
|
|
224
|
+
await sendOnDemandNotification(
|
|
225
|
+
db,
|
|
226
|
+
logger,
|
|
227
|
+
Number(userId),
|
|
228
|
+
'info',
|
|
229
|
+
'Popular Investor Data Updated',
|
|
230
|
+
`${piUsername} had their data refreshed. Portfolio, trade history, and social data have been updated.`,
|
|
231
|
+
{
|
|
232
|
+
piCid: Number(piCid),
|
|
233
|
+
piUsername: piUsername,
|
|
234
|
+
notificationType: 'watchlistAlerts'
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
collectionRegistry,
|
|
238
|
+
config,
|
|
239
|
+
notificationType: 'watchlistAlerts'
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await Promise.all(notificationPromises);
|
|
245
|
+
logger.log('INFO', `[notifyPIDataRefreshed] Sent notifications to ${userIds.length} users for PI ${piCid}`);
|
|
246
|
+
|
|
247
|
+
} catch (error) {
|
|
248
|
+
logger.log('ERROR', `[notifyPIDataRefreshed] Failed to send notifications for PI ${piCid}`, error);
|
|
249
|
+
// Don't throw - notifications are non-critical
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
86
253
|
/**
|
|
87
254
|
* Send notification when computation completes
|
|
88
255
|
*/
|
|
89
|
-
async function notifyComputationComplete(db, logger, requestingUserCid, requestId, computationName, displayName, success, error = null) {
|
|
256
|
+
async function notifyComputationComplete(db, logger, requestingUserCid, requestId, computationName, displayName, success, error = null, options = {}) {
|
|
90
257
|
if (!requestingUserCid) return; // No user to notify
|
|
91
258
|
|
|
92
259
|
const type = success ? 'success' : 'error';
|
|
@@ -102,7 +269,11 @@ async function notifyComputationComplete(db, logger, requestingUserCid, requestI
|
|
|
102
269
|
computationName,
|
|
103
270
|
displayName,
|
|
104
271
|
stage: 'computation',
|
|
105
|
-
success
|
|
272
|
+
success,
|
|
273
|
+
notificationType: 'userActionCompletions'
|
|
274
|
+
}, {
|
|
275
|
+
...options,
|
|
276
|
+
notificationType: 'userActionCompletions'
|
|
106
277
|
});
|
|
107
278
|
}
|
|
108
279
|
|
|
@@ -122,9 +293,170 @@ function getComputationDisplayName(computationName) {
|
|
|
122
293
|
return displayNames[computationName] || computationName;
|
|
123
294
|
}
|
|
124
295
|
|
|
296
|
+
/**
|
|
297
|
+
* GET /user/me/notification-preferences
|
|
298
|
+
* Get user notification preferences
|
|
299
|
+
*/
|
|
300
|
+
async function getNotificationPreferences(req, res, dependencies, config) {
|
|
301
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
302
|
+
const { userCid } = req.query;
|
|
303
|
+
|
|
304
|
+
if (!userCid) {
|
|
305
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const preferences = await getUserNotificationPreferences(db, collectionRegistry, Number(userCid), config);
|
|
310
|
+
return res.status(200).json({
|
|
311
|
+
success: true,
|
|
312
|
+
preferences
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
logger.log('ERROR', `[getNotificationPreferences] Error fetching preferences for ${userCid}`, error);
|
|
316
|
+
return res.status(500).json({ error: error.message });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* PUT /user/me/notification-preferences
|
|
322
|
+
* Update user notification preferences
|
|
323
|
+
*/
|
|
324
|
+
async function updateNotificationPreferences(req, res, dependencies, config) {
|
|
325
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
326
|
+
const { userCid } = req.query;
|
|
327
|
+
const { preferences } = req.body;
|
|
328
|
+
|
|
329
|
+
if (!userCid) {
|
|
330
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!preferences || typeof preferences !== 'object') {
|
|
334
|
+
return res.status(400).json({ error: "Missing or invalid preferences object" });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
// Validate preferences
|
|
339
|
+
const validKeys = ['syncProcesses', 'userActionCompletions', 'watchlistAlerts', 'testAlerts'];
|
|
340
|
+
const settingsData = {};
|
|
341
|
+
|
|
342
|
+
for (const key of validKeys) {
|
|
343
|
+
if (preferences.hasOwnProperty(key)) {
|
|
344
|
+
settingsData[key] = Boolean(preferences[key]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (Object.keys(settingsData).length === 0) {
|
|
349
|
+
return res.status(400).json({ error: "No valid preferences provided" });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Use writeWithMigration to support legacy paths during migration
|
|
353
|
+
await writeWithMigration(
|
|
354
|
+
db,
|
|
355
|
+
'signedInUsers',
|
|
356
|
+
'notificationPreferences',
|
|
357
|
+
{ cid: String(userCid) },
|
|
358
|
+
{
|
|
359
|
+
settings: settingsData,
|
|
360
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
isCollection: false,
|
|
364
|
+
merge: true,
|
|
365
|
+
dataType: 'notificationPreferences',
|
|
366
|
+
config,
|
|
367
|
+
documentId: 'settings',
|
|
368
|
+
collectionRegistry
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
logger.log('INFO', `[updateNotificationPreferences] Updated preferences for user ${userCid}`);
|
|
373
|
+
|
|
374
|
+
// Return updated preferences
|
|
375
|
+
const updatedPreferences = await getUserNotificationPreferences(db, collectionRegistry, Number(userCid), config);
|
|
376
|
+
|
|
377
|
+
return res.status(200).json({
|
|
378
|
+
success: true,
|
|
379
|
+
preferences: updatedPreferences
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logger.log('ERROR', `[updateNotificationPreferences] Error updating preferences for ${userCid}`, error);
|
|
383
|
+
return res.status(500).json({ error: error.message });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* GET /user/me/notifications
|
|
389
|
+
* Get user notification history
|
|
390
|
+
*/
|
|
391
|
+
async function getNotificationHistory(req, res, dependencies, config) {
|
|
392
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
393
|
+
const { userCid, limit = 50, offset = 0, type, read } = req.query;
|
|
394
|
+
|
|
395
|
+
if (!userCid) {
|
|
396
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
// Use collection registry to get notifications path
|
|
401
|
+
let notificationsPath;
|
|
402
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
403
|
+
notificationsPath = collectionRegistry.getCollectionPath('signedInUsers', 'notifications', { cid: String(userCid) });
|
|
404
|
+
} else {
|
|
405
|
+
// Fallback to legacy path
|
|
406
|
+
notificationsPath = `user_notifications/${userCid}/notifications`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let query = db.collection(notificationsPath)
|
|
410
|
+
.orderBy('timestamp', 'desc')
|
|
411
|
+
.limit(parseInt(limit));
|
|
412
|
+
|
|
413
|
+
if (type) {
|
|
414
|
+
query = query.where('type', '==', type);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (read !== undefined) {
|
|
418
|
+
query = query.where('read', '==', read === 'true');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const snapshot = await query.get();
|
|
422
|
+
const notifications = [];
|
|
423
|
+
|
|
424
|
+
snapshot.forEach(doc => {
|
|
425
|
+
const data = doc.data();
|
|
426
|
+
notifications.push({
|
|
427
|
+
id: doc.id,
|
|
428
|
+
type: data.type || 'other',
|
|
429
|
+
subType: data.subType,
|
|
430
|
+
title: data.title || '',
|
|
431
|
+
message: data.message || '',
|
|
432
|
+
read: data.read || false,
|
|
433
|
+
timestamp: data.timestamp?.toDate?.()?.toISOString() || data.createdAt?.toDate?.()?.toISOString() || new Date().toISOString(),
|
|
434
|
+
metadata: data.metadata || {}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return res.status(200).json({
|
|
439
|
+
success: true,
|
|
440
|
+
notifications,
|
|
441
|
+
count: notifications.length,
|
|
442
|
+
limit: parseInt(limit),
|
|
443
|
+
offset: parseInt(offset)
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
logger.log('ERROR', `[getNotificationHistory] Error fetching notifications for ${userCid}`, error);
|
|
447
|
+
return res.status(500).json({ error: error.message });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
125
451
|
module.exports = {
|
|
126
452
|
sendOnDemandNotification,
|
|
127
453
|
notifyTaskEngineComplete,
|
|
128
454
|
notifyComputationComplete,
|
|
129
|
-
|
|
455
|
+
notifyPIDataRefreshed,
|
|
456
|
+
getComputationDisplayName,
|
|
457
|
+
getUserNotificationPreferences,
|
|
458
|
+
shouldSendNotification,
|
|
459
|
+
getNotificationPreferences,
|
|
460
|
+
updateNotificationPreferences,
|
|
461
|
+
getNotificationHistory
|
|
130
462
|
};
|
|
@@ -13,6 +13,7 @@ const { getAlertTypes, getDynamicWatchlistComputations, getUserAlerts, getAlertC
|
|
|
13
13
|
const { requestPiFetch, getPiFetchStatus } = require('./helpers/fetch/on_demand_fetch_helpers');
|
|
14
14
|
const { requestUserSync, getUserSyncStatus } = require('./helpers/sync/user_sync_helpers');
|
|
15
15
|
const { sendTestAlert } = require('./helpers/alerts/test_alert_helpers');
|
|
16
|
+
const { getNotificationPreferences, updateNotificationPreferences, getNotificationHistory } = require('./helpers/notifications/notification_helpers');
|
|
16
17
|
|
|
17
18
|
module.exports = (dependencies, config) => {
|
|
18
19
|
const router = express.Router();
|
|
@@ -99,5 +100,10 @@ module.exports = (dependencies, config) => {
|
|
|
99
100
|
router.put('/me/alerts/read-all', (req, res) => markAllAlertsRead(req, res, dependencies, config));
|
|
100
101
|
router.delete('/me/alerts/:alertId', (req, res) => deleteAlert(req, res, dependencies, config));
|
|
101
102
|
|
|
103
|
+
// --- Notification Preferences & History ---
|
|
104
|
+
router.get('/me/notification-preferences', (req, res) => getNotificationPreferences(req, res, dependencies, config));
|
|
105
|
+
router.put('/me/notification-preferences', (req, res) => updateNotificationPreferences(req, res, dependencies, config));
|
|
106
|
+
router.get('/me/notifications', (req, res) => getNotificationHistory(req, res, dependencies, config));
|
|
107
|
+
|
|
102
108
|
return router;
|
|
103
109
|
};
|
|
@@ -515,6 +515,17 @@ async function handlePopularInvestorUpdate(taskData, config, dependencies) {
|
|
|
515
515
|
|
|
516
516
|
logger.log('SUCCESS', `[PI Update] Completed full update for ${username} (Portfolio: ✓, History: ✓, Social: ${socialFetched ? '✓' : '✗'})`);
|
|
517
517
|
|
|
518
|
+
// Send notifications to users who have this PI in their watchlist
|
|
519
|
+
if (cid && username && db && collectionRegistry && config) {
|
|
520
|
+
try {
|
|
521
|
+
const { notifyPIDataRefreshed } = require('../../../generic-api/user-api/helpers/notifications/notification_helpers');
|
|
522
|
+
await notifyPIDataRefreshed(db, logger, collectionRegistry, cid, username, config);
|
|
523
|
+
} catch (notifError) {
|
|
524
|
+
logger.log('WARN', `[PI Update] Failed to send notifications for PI ${cid}: ${notifError.message}`);
|
|
525
|
+
// Non-critical, continue
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
518
529
|
// Update request status and trigger computation if this is an on-demand request (PI fetch or sync)
|
|
519
530
|
if (requestId && (source === 'on_demand' || source === 'on_demand_sync') && db) {
|
|
520
531
|
try {
|