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.
- package/functions/alert-system/helpers/alert_helpers.js +6 -6
- package/functions/alert-system/index.js +1 -1
- package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
- package/functions/api-v2/helpers/task_engine_helper.js +51 -0
- package/functions/api-v2/index.js +36 -0
- package/functions/api-v2/middleware/identity_middleware.js +48 -0
- package/functions/api-v2/package.json +6 -0
- package/functions/api-v2/routes/alerts.js +168 -0
- package/functions/api-v2/routes/index.js +35 -0
- package/functions/api-v2/routes/notifications.js +38 -0
- package/functions/api-v2/routes/popular_investors.js +204 -0
- package/functions/api-v2/routes/profile.js +212 -0
- package/functions/api-v2/routes/reviews.js +72 -0
- package/functions/api-v2/routes/settings.js +71 -0
- package/functions/api-v2/routes/sync.js +132 -0
- package/functions/api-v2/routes/verification.js +47 -0
- package/functions/api-v2/routes/watchlists.js +148 -0
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
- package/index.js +6 -2
- package/package.json +2 -1
- package/functions/generic-api/admin-api/index.js +0 -895
- package/functions/generic-api/helpers/api_helpers.js +0 -457
- package/functions/generic-api/index.js +0 -204
- package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
- package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
- package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
- package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
- package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
- package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
- package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
- package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
- package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
- package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
- package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
- package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
- 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
|
-
};
|