bulltrackers-module 1.0.591 → 1.0.592

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.
Files changed (68) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +6 -6
  2. package/functions/alert-system/index.js +1 -1
  3. package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
  4. package/functions/api-v2/helpers/task_engine_helper.js +51 -0
  5. package/functions/api-v2/index.js +36 -0
  6. package/functions/api-v2/middleware/identity_middleware.js +48 -0
  7. package/functions/api-v2/package.json +6 -0
  8. package/functions/api-v2/routes/alerts.js +168 -0
  9. package/functions/api-v2/routes/index.js +35 -0
  10. package/functions/api-v2/routes/notifications.js +38 -0
  11. package/functions/api-v2/routes/popular_investors.js +204 -0
  12. package/functions/api-v2/routes/profile.js +212 -0
  13. package/functions/api-v2/routes/reviews.js +72 -0
  14. package/functions/api-v2/routes/settings.js +71 -0
  15. package/functions/api-v2/routes/sync.js +132 -0
  16. package/functions/api-v2/routes/verification.js +47 -0
  17. package/functions/api-v2/routes/watchlists.js +148 -0
  18. package/functions/computation-system/helpers/computation_worker.js +1 -1
  19. package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
  20. package/index.js +6 -2
  21. package/package.json +2 -1
  22. package/functions/generic-api/admin-api/index.js +0 -895
  23. package/functions/generic-api/helpers/api_helpers.js +0 -457
  24. package/functions/generic-api/index.js +0 -204
  25. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
  26. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
  27. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
  28. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
  29. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
  30. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
  31. package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
  32. package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
  33. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
  34. package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
  35. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
  36. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
  37. package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
  38. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
  39. package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
  40. package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
  41. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
  42. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
  43. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
  44. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
  45. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
  46. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
  47. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
  48. package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
  49. package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
  50. package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
  51. package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
  52. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
  53. package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
  54. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
  55. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
  56. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
  57. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
  58. package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
  59. package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
  60. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
  61. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
  62. package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
  63. package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
  64. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
  65. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
  66. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
  67. package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
  68. package/functions/generic-api/user-api/index.js +0 -109
@@ -1,641 +0,0 @@
1
- /**
2
- * @fileoverview Notification Helpers for On-Demand Requests
3
- * Sends notifications to users when their on-demand sync/computation requests complete
4
- * UPDATED: Added notification preferences support and task engine notifications
5
- */
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
- }
80
-
81
- /**
82
- * Send a notification to a user about their on-demand request
83
- * @param {object} db - Firestore instance
84
- * @param {object} logger - Logger instance
85
- * @param {number} userCid - User CID to notify
86
- * @param {string} type - Notification type ('success', 'error', 'progress')
87
- * @param {string} title - Notification title
88
- * @param {string} message - Notification message
89
- * @param {object} metadata - Additional metadata (requestId, computationName, etc.)
90
- * @param {object} options - Optional: { collectionRegistry, config, notificationType }
91
- */
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
- }
108
- try {
109
- const { collectionRegistry } = options;
110
- const config = options.config || {};
111
-
112
- const notificationId = `notif_${Date.now()}_${userCid}_${Math.random().toString(36).substring(2, 9)}`;
113
-
114
- const notificationData = {
115
- id: notificationId,
116
- type: metadata.notificationType || 'on_demand',
117
- subType: type, // 'success', 'error', 'progress', 'info'
118
- title,
119
- message,
120
- read: false,
121
- timestamp: FieldValue.serverTimestamp(),
122
- createdAt: FieldValue.serverTimestamp(),
123
- metadata: {
124
- ...metadata,
125
- userCid: Number(userCid)
126
- }
127
- };
128
-
129
- // Write using collection registry
130
- // notifications is a subcollection, so we need isCollection: true and documentId
131
- if (collectionRegistry) {
132
- await writeWithMigration(
133
- db,
134
- 'signedInUsers',
135
- 'notifications',
136
- { cid: String(userCid) },
137
- notificationData,
138
- {
139
- isCollection: true,
140
- merge: false,
141
- dataType: 'notifications',
142
- documentId: notificationId,
143
- dualWrite: false, // Disable dual write - we're fully migrated to new path
144
- config,
145
- collectionRegistry
146
- }
147
- );
148
- } else {
149
- // Fallback to legacy path
150
- const notificationRef = db.collection('user_notifications')
151
- .doc(String(userCid))
152
- .collection('notifications')
153
- .doc(notificationId);
154
- await notificationRef.set(notificationData);
155
- }
156
-
157
- logger.log('INFO', `[sendOnDemandNotification] Sent ${type} notification to user ${userCid}: ${title}`);
158
-
159
- } catch (error) {
160
- logger.log('ERROR', `[sendOnDemandNotification] Failed to send notification to user ${userCid}`, error);
161
- // Don't throw - notifications are non-critical
162
- }
163
- }
164
-
165
- /**
166
- * Send progress notification during task engine processing
167
- */
168
- async function notifyTaskEngineProgress(db, logger, requestingUserCid, requestId, username, stage, dataType = null, options = {}) {
169
- if (!requestingUserCid) return; // No user to notify
170
-
171
- let title = 'Data Sync In Progress';
172
- let message = `Syncing data for ${username}...`;
173
-
174
- if (stage === 'started') {
175
- title = 'Data Sync Started';
176
- message = `Started syncing data for ${username}. This may take a few minutes.`;
177
- } else if (stage === 'portfolio_complete') {
178
- title = 'Portfolio Data Synced';
179
- message = `Portfolio data for ${username} has been fetched and stored.`;
180
- } else if (stage === 'history_complete') {
181
- title = 'Trade History Synced';
182
- message = `Trade history for ${username} has been fetched and stored.`;
183
- } else if (stage === 'social_complete') {
184
- title = 'Social Posts Synced';
185
- message = `Social posts for ${username} have been fetched and stored.`;
186
- } else if (stage === 'indexing') {
187
- title = 'Indexing Data';
188
- message = `Indexing data for ${username}...`;
189
- } else if (stage === 'computing') {
190
- title = 'Computing Metrics';
191
- message = `Computing metrics for ${username}...`;
192
- }
193
-
194
- await sendOnDemandNotification(db, logger, requestingUserCid, 'progress', title, message, {
195
- requestId,
196
- username,
197
- stage,
198
- dataType,
199
- notificationType: 'syncProcesses'
200
- }, {
201
- ...options,
202
- notificationType: 'syncProcesses'
203
- });
204
- }
205
-
206
- /**
207
- * Send notification when task engine completes data fetch
208
- */
209
- async function notifyTaskEngineComplete(db, logger, requestingUserCid, requestId, username, success, error = null, options = {}) {
210
- if (!requestingUserCid) return; // No user to notify
211
-
212
- const type = success ? 'success' : 'error';
213
- const title = success
214
- ? 'Data Sync Complete'
215
- : 'Data Sync Failed';
216
-
217
- // Build a more detailed message based on what succeeded
218
- let message = '';
219
- if (success && typeof success === 'object') {
220
- const completed = [];
221
- if (success.portfolio) completed.push('portfolio');
222
- if (success.history) completed.push('trade history');
223
- if (success.social) completed.push('social posts');
224
-
225
- if (completed.length > 0) {
226
- message = `Your data sync for ${username} has completed. ${completed.join(', ')} data ${completed.length === 1 ? 'has' : 'have'} been stored.`;
227
- } else {
228
- message = `Your data sync for ${username} has completed, but no data was stored.`;
229
- }
230
- } else if (success) {
231
- message = `Your data sync for ${username} has completed. Portfolio, history, and social data have been stored.`;
232
- } else {
233
- message = error || 'An error occurred while syncing your data. Please try again.';
234
- }
235
-
236
- await sendOnDemandNotification(db, logger, requestingUserCid, type, title, message, {
237
- requestId,
238
- username,
239
- stage: 'task_engine',
240
- success: typeof success === 'object' ? success : { portfolio: success, history: success, social: success },
241
- notificationType: 'syncProcesses'
242
- }, {
243
- ...options,
244
- notificationType: 'syncProcesses'
245
- });
246
- }
247
-
248
- /**
249
- * Send notification when PI data is refreshed (for users with PI in watchlist)
250
- * @param {object} db - Firestore instance
251
- * @param {object} logger - Logger instance
252
- * @param {object} collectionRegistry - Collection registry
253
- * @param {number} piCid - Popular Investor CID
254
- * @param {string} piUsername - Popular Investor username
255
- * @param {object} config - Config object
256
- */
257
- async function notifyPIDataRefreshed(db, logger, collectionRegistry, piCid, piUsername, config) {
258
- try {
259
- // Find all users who have this PI in their watchlist
260
- // Check both static and dynamic watchlists
261
- const watchlistMembershipRef = db.collection('WatchlistMembershipData')
262
- .doc(new Date().toISOString().split('T')[0]);
263
-
264
- const membershipDoc = await watchlistMembershipRef.get();
265
- if (!membershipDoc.exists) {
266
- logger.log('INFO', `[notifyPIDataRefreshed] No watchlist membership data found for today`);
267
- return;
268
- }
269
-
270
- const membershipData = membershipDoc.data();
271
- const piCidStr = String(piCid);
272
- const piMembership = membershipData[piCidStr];
273
-
274
- if (!piMembership || !piMembership.users || !Array.isArray(piMembership.users)) {
275
- logger.log('INFO', `[notifyPIDataRefreshed] No users have PI ${piCid} in their watchlist`);
276
- return;
277
- }
278
-
279
- const userIds = piMembership.users.map(u => String(u));
280
- logger.log('INFO', `[notifyPIDataRefreshed] Found ${userIds.length} users with PI ${piCid} in watchlist`);
281
-
282
- // Send notification to each user
283
- const notificationPromises = userIds.map(async (userId) => {
284
- await sendOnDemandNotification(
285
- db,
286
- logger,
287
- Number(userId),
288
- 'info',
289
- 'Popular Investor Data Updated',
290
- `${piUsername} had their data refreshed. Portfolio, trade history, and social data have been updated.`,
291
- {
292
- piCid: Number(piCid),
293
- piUsername: piUsername,
294
- notificationType: 'watchlistAlerts'
295
- },
296
- {
297
- collectionRegistry,
298
- config,
299
- notificationType: 'watchlistAlerts'
300
- }
301
- );
302
- });
303
-
304
- await Promise.all(notificationPromises);
305
- logger.log('INFO', `[notifyPIDataRefreshed] Sent notifications to ${userIds.length} users for PI ${piCid}`);
306
-
307
- } catch (error) {
308
- logger.log('ERROR', `[notifyPIDataRefreshed] Failed to send notifications for PI ${piCid}`, error);
309
- // Don't throw - notifications are non-critical
310
- }
311
- }
312
-
313
- /**
314
- * Send notification when computation completes
315
- */
316
- async function notifyComputationComplete(db, logger, requestingUserCid, requestId, computationName, displayName, success, error = null, options = {}) {
317
- if (!requestingUserCid) return; // No user to notify
318
-
319
- const type = success ? 'success' : 'error';
320
- const title = success
321
- ? 'Computation Complete'
322
- : 'Computation Failed';
323
- const message = success
324
- ? `${displayName || computationName} has been computed and stored.`
325
- : (error || `Failed to compute ${displayName || computationName}. Please try again.`);
326
-
327
- await sendOnDemandNotification(db, logger, requestingUserCid, type, title, message, {
328
- requestId,
329
- computationName,
330
- displayName,
331
- stage: 'computation',
332
- success,
333
- notificationType: 'userActionCompletions'
334
- }, {
335
- ...options,
336
- notificationType: 'userActionCompletions'
337
- });
338
- }
339
-
340
- /**
341
- * Get human-readable name for a computation
342
- */
343
- function getComputationDisplayName(computationName) {
344
- const displayNames = {
345
- 'SignedInUserProfileMetrics': 'Profile Metrics',
346
- 'SignedInUserCopiedList': 'Copied Investors List',
347
- 'SignedInUserCopiedPIs': 'Copied Popular Investors',
348
- 'SignedInUserPastCopies': 'Past Copy History',
349
- 'PopularInvestorProfileMetrics': 'Popular Investor Profile',
350
- 'SignedInUserPIPersonalizedMetrics': 'Personalized Metrics'
351
- };
352
-
353
- return displayNames[computationName] || computationName;
354
- }
355
-
356
- /**
357
- * GET /user/me/notification-preferences
358
- * Get user notification preferences
359
- */
360
- async function getNotificationPreferences(req, res, dependencies, config) {
361
- const { db, logger, collectionRegistry } = dependencies;
362
- const { userCid } = req.query;
363
-
364
- if (!userCid) {
365
- return res.status(400).json({ error: "Missing userCid" });
366
- }
367
-
368
- try {
369
- const preferences = await getUserNotificationPreferences(db, collectionRegistry, Number(userCid), config);
370
- return res.status(200).json({
371
- success: true,
372
- preferences
373
- });
374
- } catch (error) {
375
- logger.log('ERROR', `[getNotificationPreferences] Error fetching preferences for ${userCid}`, error);
376
- return res.status(500).json({ error: error.message });
377
- }
378
- }
379
-
380
- /**
381
- * PUT /user/me/notification-preferences
382
- * Update user notification preferences
383
- */
384
- async function updateNotificationPreferences(req, res, dependencies, config) {
385
- const { db, logger, collectionRegistry } = dependencies;
386
- const { userCid } = req.query;
387
- const { preferences } = req.body;
388
-
389
- if (!userCid) {
390
- return res.status(400).json({ error: "Missing userCid" });
391
- }
392
-
393
- if (!preferences || typeof preferences !== 'object') {
394
- return res.status(400).json({ error: "Missing or invalid preferences object" });
395
- }
396
-
397
- try {
398
- // Validate preferences
399
- const validKeys = ['syncProcesses', 'userActionCompletions', 'watchlistAlerts', 'testAlerts'];
400
- const settingsData = {};
401
-
402
- for (const key of validKeys) {
403
- if (preferences.hasOwnProperty(key)) {
404
- settingsData[key] = Boolean(preferences[key]);
405
- }
406
- }
407
-
408
- if (Object.keys(settingsData).length === 0) {
409
- return res.status(400).json({ error: "No valid preferences provided" });
410
- }
411
-
412
- // Use writeWithMigration to support legacy paths during migration
413
- await writeWithMigration(
414
- db,
415
- 'signedInUsers',
416
- 'notificationPreferences',
417
- { cid: String(userCid) },
418
- {
419
- settings: settingsData,
420
- updatedAt: FieldValue.serverTimestamp()
421
- },
422
- {
423
- isCollection: false,
424
- merge: true,
425
- dataType: 'notificationPreferences',
426
- config,
427
- documentId: 'settings',
428
- collectionRegistry
429
- }
430
- );
431
-
432
- logger.log('INFO', `[updateNotificationPreferences] Updated preferences for user ${userCid}`);
433
-
434
- // Return updated preferences
435
- const updatedPreferences = await getUserNotificationPreferences(db, collectionRegistry, Number(userCid), config);
436
-
437
- return res.status(200).json({
438
- success: true,
439
- preferences: updatedPreferences
440
- });
441
- } catch (error) {
442
- logger.log('ERROR', `[updateNotificationPreferences] Error updating preferences for ${userCid}`, error);
443
- return res.status(500).json({ error: error.message });
444
- }
445
- }
446
-
447
- /**
448
- * GET /user/me/notifications
449
- * Get user notification history with pagination, date range, and filtering
450
- */
451
- async function getNotificationHistory(req, res, dependencies, config) {
452
- const { db, logger, collectionRegistry } = dependencies;
453
- const {
454
- userCid,
455
- limit = 50,
456
- offset = 0,
457
- type,
458
- read,
459
- subType,
460
- alertType,
461
- startDate,
462
- endDate
463
- } = req.query;
464
-
465
- if (!userCid) {
466
- return res.status(400).json({ error: "Missing userCid" });
467
- }
468
-
469
- try {
470
- // Use collection registry to get notifications path
471
- let notificationsPath;
472
- if (collectionRegistry && collectionRegistry.getCollectionPath) {
473
- notificationsPath = collectionRegistry.getCollectionPath('signedInUsers', 'notifications', { cid: String(userCid) });
474
- } else {
475
- // Fallback to legacy path
476
- notificationsPath = `user_notifications/${userCid}/notifications`;
477
- }
478
-
479
- // Parse date filters
480
- let startDateObj = null;
481
- let endDateObj = null;
482
- if (startDate) {
483
- startDateObj = new Date(startDate);
484
- if (isNaN(startDateObj.getTime())) {
485
- return res.status(400).json({ error: "Invalid startDate format" });
486
- }
487
- }
488
- if (endDate) {
489
- endDateObj = new Date(endDate);
490
- if (isNaN(endDateObj.getTime())) {
491
- return res.status(400).json({ error: "Invalid endDate format" });
492
- }
493
- // Set to end of day
494
- endDateObj.setHours(23, 59, 59, 999);
495
- }
496
-
497
- // Build query - fetch more than needed to apply filters in memory
498
- // Firestore has limitations on complex queries, so we'll fetch a larger batch
499
- // and filter in memory for date range and subType
500
- const maxFetchLimit = 1000; // Fetch up to 1000 to apply filters
501
- let query = db.collection(notificationsPath)
502
- .orderBy('timestamp', 'desc')
503
- .limit(maxFetchLimit);
504
-
505
- if (type) {
506
- query = query.where('type', '==', type);
507
- }
508
-
509
- if (read !== undefined) {
510
- query = query.where('read', '==', read === 'true');
511
- }
512
-
513
- let snapshot;
514
- try {
515
- snapshot = await query.get();
516
- } catch (queryError) {
517
- // If query fails (e.g., missing index), try without filters
518
- logger.log('WARN', `[getNotificationHistory] Query failed, trying without filters: ${queryError.message}`);
519
- query = db.collection(notificationsPath)
520
- .orderBy('timestamp', 'desc')
521
- .limit(maxFetchLimit);
522
- snapshot = await query.get();
523
- }
524
-
525
- const notifications = [];
526
- const admin = require('firebase-admin');
527
-
528
- snapshot.forEach(doc => {
529
- const data = doc.data();
530
-
531
- // Convert timestamp to Date object
532
- let timestamp = null;
533
- if (data.timestamp) {
534
- if (data.timestamp.toDate) {
535
- timestamp = data.timestamp.toDate();
536
- } else if (data.timestamp instanceof admin.firestore.Timestamp) {
537
- timestamp = data.timestamp.toDate();
538
- } else if (data.timestamp instanceof Date) {
539
- timestamp = data.timestamp;
540
- }
541
- } else if (data.createdAt) {
542
- if (data.createdAt.toDate) {
543
- timestamp = data.createdAt.toDate();
544
- } else if (data.createdAt instanceof admin.firestore.Timestamp) {
545
- timestamp = data.createdAt.toDate();
546
- } else if (data.createdAt instanceof Date) {
547
- timestamp = data.createdAt;
548
- }
549
- }
550
-
551
- if (!timestamp) {
552
- timestamp = new Date();
553
- }
554
-
555
- // Apply filters in memory
556
- let include = true;
557
-
558
- // Type filter (already applied in query if possible)
559
- if (type && data.type !== type) {
560
- include = false;
561
- }
562
-
563
- // Read filter (already applied in query if possible)
564
- if (read !== undefined && data.read !== (read === 'true')) {
565
- include = false;
566
- }
567
-
568
- // SubType filter
569
- if (subType && data.subType !== subType) {
570
- include = false;
571
- }
572
-
573
- // AlertType filter (for watchlist alerts, filters by metadata.alertType)
574
- if (alertType) {
575
- const metadata = data.metadata || {};
576
- if (metadata.alertType !== alertType) {
577
- include = false;
578
- }
579
- }
580
-
581
- // Date range filter
582
- if (startDateObj && timestamp < startDateObj) {
583
- include = false;
584
- }
585
- if (endDateObj && timestamp > endDateObj) {
586
- include = false;
587
- }
588
-
589
- if (include) {
590
- notifications.push({
591
- id: doc.id,
592
- type: data.type || 'other',
593
- subType: data.subType,
594
- title: data.title || '',
595
- message: data.message || '',
596
- read: data.read || false,
597
- timestamp: timestamp.toISOString(),
598
- metadata: data.metadata || {}
599
- });
600
- }
601
- });
602
-
603
- // Sort by timestamp (descending) to ensure proper ordering
604
- notifications.sort((a, b) => {
605
- return new Date(b.timestamp) - new Date(a.timestamp);
606
- });
607
-
608
- // Apply pagination
609
- const offsetNum = parseInt(offset);
610
- const limitNum = parseInt(limit);
611
- const totalCount = notifications.length;
612
- const paginatedNotifications = notifications.slice(offsetNum, offsetNum + limitNum);
613
-
614
- return res.status(200).json({
615
- success: true,
616
- notifications: paginatedNotifications,
617
- count: paginatedNotifications.length,
618
- total: totalCount,
619
- limit: limitNum,
620
- offset: offsetNum,
621
- hasMore: offsetNum + limitNum < totalCount
622
- });
623
- } catch (error) {
624
- logger.log('ERROR', `[getNotificationHistory] Error fetching notifications for ${userCid}`, error);
625
- return res.status(500).json({ error: error.message });
626
- }
627
- }
628
-
629
- module.exports = {
630
- sendOnDemandNotification,
631
- notifyTaskEngineProgress,
632
- notifyTaskEngineComplete,
633
- notifyComputationComplete,
634
- notifyPIDataRefreshed,
635
- getComputationDisplayName,
636
- getUserNotificationPreferences,
637
- shouldSendNotification,
638
- getNotificationPreferences,
639
- updateNotificationPreferences,
640
- getNotificationHistory
641
- };