bulltrackers-module 1.0.592 → 1.0.593

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 (36) hide show
  1. package/functions/old-generic-api/admin-api/index.js +895 -0
  2. package/functions/old-generic-api/helpers/api_helpers.js +457 -0
  3. package/functions/old-generic-api/index.js +204 -0
  4. package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +355 -0
  5. package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  6. package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +212 -0
  7. package/functions/old-generic-api/user-api/helpers/collection_helpers.js +193 -0
  8. package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  9. package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +256 -0
  10. package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +640 -0
  11. package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +195 -0
  12. package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +503 -0
  13. package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  14. package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +245 -0
  15. package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +174 -0
  16. package/functions/old-generic-api/user-api/helpers/data_helpers.js +87 -0
  17. package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +336 -0
  18. package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +615 -0
  19. package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +231 -0
  20. package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +641 -0
  21. package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +182 -0
  22. package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +137 -0
  23. package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +190 -0
  24. package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +66 -0
  25. package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +550 -0
  26. package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +378 -0
  27. package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +295 -0
  28. package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +162 -0
  29. package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +677 -0
  30. package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +323 -0
  31. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +96 -0
  32. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +141 -0
  33. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +310 -0
  34. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +829 -0
  35. package/functions/old-generic-api/user-api/index.js +109 -0
  36. package/package.json +2 -2
@@ -0,0 +1,355 @@
1
+ /**
2
+ * @fileoverview Alert Management API Helpers
3
+ * Handles fetching and managing user alerts
4
+ */
5
+
6
+ const { FieldValue } = require('@google-cloud/firestore');
7
+ const { getAllAlertTypes, getAlertType } = require('../../../../alert-system/helpers/alert_type_registry');
8
+
9
+ /**
10
+ * GET /user/me/alert-types
11
+ * Get all available alert types
12
+ */
13
+ async function getAlertTypes(req, res, dependencies, config) {
14
+ try {
15
+ const alertTypes = getAllAlertTypes();
16
+
17
+ return res.status(200).json({
18
+ success: true,
19
+ alertTypes: alertTypes.map(type => ({
20
+ id: type.id,
21
+ name: type.name,
22
+ description: type.description,
23
+ severity: type.severity,
24
+ computationName: type.computationName // Include computation name for dynamic watchlists
25
+ }))
26
+ });
27
+ } catch (error) {
28
+ const { logger } = dependencies;
29
+ logger.log('ERROR', '[getAlertTypes] Error fetching alert types', error);
30
+ return res.status(500).json({ error: error.message });
31
+ }
32
+ }
33
+
34
+ /**
35
+ * GET /user/me/dynamic-watchlist-computations
36
+ * Get available computations for dynamic watchlists (from alert types)
37
+ */
38
+ async function getDynamicWatchlistComputations(req, res, dependencies, config) {
39
+ try {
40
+ const alertTypes = getAllAlertTypes();
41
+
42
+ // Extract unique computations from alert types
43
+ const computations = alertTypes.map(type => ({
44
+ computationName: type.computationName,
45
+ alertTypeName: type.name,
46
+ description: type.description,
47
+ severity: type.severity
48
+ }));
49
+
50
+ return res.status(200).json({
51
+ success: true,
52
+ computations
53
+ });
54
+ } catch (error) {
55
+ const { logger } = dependencies;
56
+ logger.log('ERROR', '[getDynamicWatchlistComputations] Error fetching computations', error);
57
+ return res.status(500).json({ error: error.message });
58
+ }
59
+ }
60
+
61
+ /**
62
+ * GET /user/me/alerts
63
+ * Get user's alerts (paginated)
64
+ */
65
+ async function getUserAlerts(req, res, dependencies, config) {
66
+ const { db, logger } = dependencies;
67
+ const { userCid } = req.query;
68
+ const { unreadOnly, limit = 50, offset = 0, alertType } = req.query;
69
+
70
+ if (!userCid) {
71
+ return res.status(400).json({ error: "Missing userCid" });
72
+ }
73
+
74
+ try {
75
+ const alertsRef = db.collection('user_alerts')
76
+ .doc(String(userCid))
77
+ .collection('alerts');
78
+
79
+ let query = alertsRef.orderBy('createdAt', 'desc');
80
+
81
+ // Filter by read status
82
+ if (unreadOnly === 'true') {
83
+ query = query.where('read', '==', false);
84
+ }
85
+
86
+ // Filter by alert type
87
+ if (alertType) {
88
+ query = query.where('alertType', '==', alertType);
89
+ }
90
+
91
+ // Apply pagination
92
+ query = query.limit(Number(limit)).offset(Number(offset));
93
+
94
+ const snapshot = await query.get();
95
+ const alerts = [];
96
+
97
+ snapshot.forEach(doc => {
98
+ const data = doc.data();
99
+ alerts.push({
100
+ id: doc.id,
101
+ ...data,
102
+ createdAt: data.createdAt?.toDate?.() || data.createdAt,
103
+ readAt: data.readAt?.toDate?.() || data.readAt
104
+ });
105
+ });
106
+
107
+ return res.status(200).json({
108
+ success: true,
109
+ alerts: alerts,
110
+ count: alerts.length,
111
+ limit: Number(limit),
112
+ offset: Number(offset)
113
+ });
114
+ } catch (error) {
115
+ logger.log('ERROR', `[getUserAlerts] Error fetching alerts for user ${userCid}`, error);
116
+ return res.status(500).json({ error: error.message });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * GET /user/me/alerts/count
122
+ * Get unread alert count
123
+ */
124
+ async function getAlertCount(req, res, dependencies, config) {
125
+ const { db, logger } = dependencies;
126
+ const { userCid } = req.query;
127
+
128
+ if (!userCid) {
129
+ return res.status(400).json({ error: "Missing userCid" });
130
+ }
131
+
132
+ try {
133
+ // Get today's counter
134
+ const today = new Date().toISOString().split('T')[0];
135
+ const counterRef = db.collection('user_alerts')
136
+ .doc(String(userCid))
137
+ .collection('counters')
138
+ .doc(today);
139
+
140
+ const counterDoc = await counterRef.get();
141
+
142
+ if (counterDoc.exists) {
143
+ const data = counterDoc.data();
144
+ return res.status(200).json({
145
+ success: true,
146
+ unreadCount: data.unreadCount || 0,
147
+ totalCount: data.totalCount || 0,
148
+ byType: data.byType || {}
149
+ });
150
+ }
151
+
152
+ // If no counter exists, count unread alerts directly
153
+ const unreadSnapshot = await db.collection('user_alerts')
154
+ .doc(String(userCid))
155
+ .collection('alerts')
156
+ .where('read', '==', false)
157
+ .get();
158
+
159
+ return res.status(200).json({
160
+ success: true,
161
+ unreadCount: unreadSnapshot.size,
162
+ totalCount: unreadSnapshot.size,
163
+ byType: {}
164
+ });
165
+ } catch (error) {
166
+ logger.log('ERROR', `[getAlertCount] Error fetching alert count for user ${userCid}`, error);
167
+ return res.status(500).json({ error: error.message });
168
+ }
169
+ }
170
+
171
+ /**
172
+ * PUT /user/me/alerts/:alertId/read
173
+ * Mark alert as read
174
+ */
175
+ async function markAlertRead(req, res, dependencies, config) {
176
+ const { db, logger } = dependencies;
177
+ const { userCid } = req.query;
178
+ const { alertId } = req.params;
179
+
180
+ if (!userCid || !alertId) {
181
+ return res.status(400).json({ error: "Missing userCid or alertId" });
182
+ }
183
+
184
+ try {
185
+ const alertRef = db.collection('user_alerts')
186
+ .doc(String(userCid))
187
+ .collection('alerts')
188
+ .doc(alertId);
189
+
190
+ const alertDoc = await alertRef.get();
191
+
192
+ if (!alertDoc.exists) {
193
+ return res.status(404).json({ error: "Alert not found" });
194
+ }
195
+
196
+ const alertData = alertDoc.data();
197
+
198
+ // Only update if not already read
199
+ if (!alertData.read) {
200
+ await alertRef.update({
201
+ read: true,
202
+ readAt: FieldValue.serverTimestamp()
203
+ });
204
+
205
+ // Update counter
206
+ const today = new Date().toISOString().split('T')[0];
207
+ const counterRef = db.collection('user_alerts')
208
+ .doc(String(userCid))
209
+ .collection('counters')
210
+ .doc(today);
211
+
212
+ await counterRef.set({
213
+ unreadCount: FieldValue.increment(-1),
214
+ lastUpdated: FieldValue.serverTimestamp()
215
+ }, { merge: true });
216
+ }
217
+
218
+ return res.status(200).json({
219
+ success: true,
220
+ message: "Alert marked as read"
221
+ });
222
+ } catch (error) {
223
+ logger.log('ERROR', `[markAlertRead] Error marking alert ${alertId} as read`, error);
224
+ return res.status(500).json({ error: error.message });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * PUT /user/me/alerts/read-all
230
+ * Mark all alerts as read
231
+ */
232
+ async function markAllAlertsRead(req, res, dependencies, config) {
233
+ const { db, logger } = dependencies;
234
+ const { userCid } = req.query;
235
+
236
+ if (!userCid) {
237
+ return res.status(400).json({ error: "Missing userCid" });
238
+ }
239
+
240
+ try {
241
+ const alertsRef = db.collection('user_alerts')
242
+ .doc(String(userCid))
243
+ .collection('alerts');
244
+
245
+ const unreadSnapshot = await alertsRef
246
+ .where('read', '==', false)
247
+ .get();
248
+
249
+ if (unreadSnapshot.empty) {
250
+ return res.status(200).json({
251
+ success: true,
252
+ message: "No unread alerts",
253
+ updated: 0
254
+ });
255
+ }
256
+
257
+ const batch = db.batch();
258
+ let count = 0;
259
+
260
+ unreadSnapshot.forEach(doc => {
261
+ batch.update(doc.ref, {
262
+ read: true,
263
+ readAt: FieldValue.serverTimestamp()
264
+ });
265
+ count++;
266
+ });
267
+
268
+ await batch.commit();
269
+
270
+ // Reset counter
271
+ const today = new Date().toISOString().split('T')[0];
272
+ const counterRef = db.collection('user_alerts')
273
+ .doc(String(userCid))
274
+ .collection('counters')
275
+ .doc(today);
276
+
277
+ await counterRef.set({
278
+ unreadCount: 0,
279
+ lastUpdated: FieldValue.serverTimestamp()
280
+ }, { merge: true });
281
+
282
+ return res.status(200).json({
283
+ success: true,
284
+ message: "All alerts marked as read",
285
+ updated: count
286
+ });
287
+ } catch (error) {
288
+ logger.log('ERROR', `[markAllAlertsRead] Error marking all alerts as read for user ${userCid}`, error);
289
+ return res.status(500).json({ error: error.message });
290
+ }
291
+ }
292
+
293
+ /**
294
+ * DELETE /user/me/alerts/:alertId
295
+ * Delete specific alert
296
+ */
297
+ async function deleteAlert(req, res, dependencies, config) {
298
+ const { db, logger } = dependencies;
299
+ const { userCid } = req.query;
300
+ const { alertId } = req.params;
301
+
302
+ if (!userCid || !alertId) {
303
+ return res.status(400).json({ error: "Missing userCid or alertId" });
304
+ }
305
+
306
+ try {
307
+ const alertRef = db.collection('user_alerts')
308
+ .doc(String(userCid))
309
+ .collection('alerts')
310
+ .doc(alertId);
311
+
312
+ const alertDoc = await alertRef.get();
313
+
314
+ if (!alertDoc.exists) {
315
+ return res.status(404).json({ error: "Alert not found" });
316
+ }
317
+
318
+ await alertRef.delete();
319
+
320
+ // Update counter if alert was unread
321
+ const alertData = alertDoc.data();
322
+ if (!alertData.read) {
323
+ const today = new Date().toISOString().split('T')[0];
324
+ const counterRef = db.collection('user_alerts')
325
+ .doc(String(userCid))
326
+ .collection('counters')
327
+ .doc(today);
328
+
329
+ await counterRef.set({
330
+ unreadCount: FieldValue.increment(-1),
331
+ totalCount: FieldValue.increment(-1),
332
+ lastUpdated: FieldValue.serverTimestamp()
333
+ }, { merge: true });
334
+ }
335
+
336
+ return res.status(200).json({
337
+ success: true,
338
+ message: "Alert deleted"
339
+ });
340
+ } catch (error) {
341
+ logger.log('ERROR', `[deleteAlert] Error deleting alert ${alertId}`, error);
342
+ return res.status(500).json({ error: error.message });
343
+ }
344
+ }
345
+
346
+ module.exports = {
347
+ getAlertTypes,
348
+ getDynamicWatchlistComputations,
349
+ getUserAlerts,
350
+ getAlertCount,
351
+ markAlertRead,
352
+ markAllAlertsRead,
353
+ deleteAlert
354
+ };
355
+
@@ -0,0 +1,327 @@
1
+ /**
2
+ * @fileoverview Alert Subscription Management Helpers
3
+ * Handles subscriptions for watchlist alerts (static and dynamic)
4
+ */
5
+
6
+ const { FieldValue } = require('@google-cloud/firestore');
7
+
8
+ /**
9
+ * POST /user/me/subscriptions
10
+ * Subscribe to alerts for a PI in a watchlist
11
+ */
12
+ async function subscribeToAlerts(req, res, dependencies, config) {
13
+ const { db, logger } = dependencies;
14
+ const { userCid, watchlistId, piCid, alertTypes, thresholds } = req.body;
15
+
16
+ if (!userCid || !watchlistId || !piCid) {
17
+ return res.status(400).json({ error: "Missing required fields: userCid, watchlistId, piCid" });
18
+ }
19
+
20
+ try {
21
+ // Verify watchlist exists and belongs to user
22
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
23
+ const watchlistRef = db.collection(watchlistsCollection)
24
+ .doc(String(userCid))
25
+ .collection('lists')
26
+ .doc(watchlistId);
27
+
28
+ const watchlistDoc = await watchlistRef.get();
29
+
30
+ if (!watchlistDoc.exists) {
31
+ return res.status(404).json({ error: "Watchlist not found" });
32
+ }
33
+
34
+ const watchlistData = watchlistDoc.data();
35
+
36
+ // Verify PI is in the watchlist
37
+ let piInWatchlist = false;
38
+ if (watchlistData.type === 'static') {
39
+ piInWatchlist = watchlistData.items?.some(item => item.cid === Number(piCid));
40
+ } else if (watchlistData.type === 'dynamic') {
41
+ // For dynamic watchlists, we'll check if the PI is in the current computation result
42
+ // This is a simplified check - in production, you'd fetch the latest computation result
43
+ piInWatchlist = true; // Allow subscriptions for dynamic watchlists
44
+ }
45
+
46
+ if (!piInWatchlist && watchlistData.type === 'static') {
47
+ return res.status(400).json({ error: "PI is not in this watchlist" });
48
+ }
49
+
50
+ // Default alert types (all enabled) if not provided
51
+ const defaultAlertTypes = {
52
+ newPositions: true,
53
+ volatilityChanges: true,
54
+ increasedRisk: true,
55
+ newSector: true,
56
+ increasedPositionSize: true,
57
+ newSocialPost: true
58
+ };
59
+
60
+ const subscriptionData = {
61
+ userCid: Number(userCid),
62
+ piCid: Number(piCid),
63
+ watchlistId: watchlistId,
64
+ alertTypes: alertTypes || defaultAlertTypes,
65
+ thresholds: thresholds || {},
66
+ subscribedAt: FieldValue.serverTimestamp(),
67
+ lastAlertAt: null
68
+ };
69
+
70
+ // Store subscription
71
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
72
+ const subscriptionRef = db.collection(subscriptionsCollection)
73
+ .doc(String(userCid))
74
+ .collection('alerts')
75
+ .doc(String(piCid));
76
+
77
+ await subscriptionRef.set(subscriptionData, { merge: true });
78
+
79
+ logger.log('SUCCESS', `[subscribeToAlerts] User ${userCid} subscribed to alerts for PI ${piCid} in watchlist ${watchlistId}`);
80
+
81
+ return res.status(200).json({
82
+ success: true,
83
+ subscription: subscriptionData
84
+ });
85
+
86
+ } catch (error) {
87
+ logger.log('ERROR', `[subscribeToAlerts] Error creating subscription for user ${userCid}`, error);
88
+ return res.status(500).json({ error: error.message });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * PUT /user/me/subscriptions/:piCid
94
+ * Update alert subscription settings
95
+ */
96
+ async function updateSubscription(req, res, dependencies, config) {
97
+ const { db, logger } = dependencies;
98
+ const { userCid } = req.query;
99
+ const { piCid } = req.params;
100
+ const { alertTypes, thresholds } = req.body;
101
+
102
+ if (!userCid || !piCid) {
103
+ return res.status(400).json({ error: "Missing userCid or piCid" });
104
+ }
105
+
106
+ try {
107
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
108
+ const subscriptionRef = db.collection(subscriptionsCollection)
109
+ .doc(String(userCid))
110
+ .collection('alerts')
111
+ .doc(String(piCid));
112
+
113
+ const subscriptionDoc = await subscriptionRef.get();
114
+
115
+ if (!subscriptionDoc.exists) {
116
+ return res.status(404).json({ error: "Subscription not found" });
117
+ }
118
+
119
+ const updates = {};
120
+
121
+ if (alertTypes !== undefined) {
122
+ updates.alertTypes = alertTypes;
123
+ }
124
+
125
+ if (thresholds !== undefined) {
126
+ updates.thresholds = thresholds;
127
+ }
128
+
129
+ if (Object.keys(updates).length === 0) {
130
+ return res.status(400).json({ error: "No updates provided" });
131
+ }
132
+
133
+ updates.updatedAt = FieldValue.serverTimestamp();
134
+
135
+ await subscriptionRef.update(updates);
136
+
137
+ logger.log('SUCCESS', `[updateSubscription] Updated subscription for user ${userCid}, PI ${piCid}`);
138
+
139
+ const updatedDoc = await subscriptionRef.get();
140
+ return res.status(200).json({
141
+ success: true,
142
+ subscription: {
143
+ id: updatedDoc.id,
144
+ ...updatedDoc.data()
145
+ }
146
+ });
147
+
148
+ } catch (error) {
149
+ logger.log('ERROR', `[updateSubscription] Error updating subscription for user ${userCid}`, error);
150
+ return res.status(500).json({ error: error.message });
151
+ }
152
+ }
153
+
154
+ /**
155
+ * DELETE /user/me/subscriptions/:piCid
156
+ * Unsubscribe from alerts for a PI
157
+ */
158
+ async function unsubscribeFromAlerts(req, res, dependencies, config) {
159
+ const { db, logger } = dependencies;
160
+ const { userCid } = req.query;
161
+ const { piCid } = req.params;
162
+
163
+ if (!userCid || !piCid) {
164
+ return res.status(400).json({ error: "Missing userCid or piCid" });
165
+ }
166
+
167
+ try {
168
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
169
+ const subscriptionRef = db.collection(subscriptionsCollection)
170
+ .doc(String(userCid))
171
+ .collection('alerts')
172
+ .doc(String(piCid));
173
+
174
+ const subscriptionDoc = await subscriptionRef.get();
175
+
176
+ if (!subscriptionDoc.exists) {
177
+ return res.status(404).json({ error: "Subscription not found" });
178
+ }
179
+
180
+ await subscriptionRef.delete();
181
+
182
+ logger.log('SUCCESS', `[unsubscribeFromAlerts] User ${userCid} unsubscribed from alerts for PI ${piCid}`);
183
+
184
+ return res.status(200).json({
185
+ success: true,
186
+ message: "Unsubscribed successfully"
187
+ });
188
+
189
+ } catch (error) {
190
+ logger.log('ERROR', `[unsubscribeFromAlerts] Error unsubscribing user ${userCid} from PI ${piCid}`, error);
191
+ return res.status(500).json({ error: error.message });
192
+ }
193
+ }
194
+
195
+ /**
196
+ * GET /user/me/subscriptions
197
+ * Get all subscriptions for a user
198
+ */
199
+ async function getUserSubscriptions(req, res, dependencies, config) {
200
+ const { db, logger } = dependencies;
201
+ const { userCid } = req.query;
202
+
203
+ if (!userCid) {
204
+ return res.status(400).json({ error: "Missing userCid" });
205
+ }
206
+
207
+ try {
208
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
209
+ const subscriptionsRef = db.collection(subscriptionsCollection)
210
+ .doc(String(userCid))
211
+ .collection('alerts');
212
+
213
+ const snapshot = await subscriptionsRef.get();
214
+
215
+ const subscriptions = [];
216
+ snapshot.forEach(doc => {
217
+ subscriptions.push({
218
+ piCid: Number(doc.id),
219
+ ...doc.data()
220
+ });
221
+ });
222
+
223
+ return res.status(200).json({
224
+ subscriptions,
225
+ count: subscriptions.length
226
+ });
227
+
228
+ } catch (error) {
229
+ logger.log('ERROR', `[getUserSubscriptions] Error fetching subscriptions for user ${userCid}`, error);
230
+ return res.status(500).json({ error: error.message });
231
+ }
232
+ }
233
+
234
+ /**
235
+ * POST /user/me/watchlists/:id/subscribe-all
236
+ * Subscribe to all PIs in a watchlist with default alert settings
237
+ */
238
+ async function subscribeToWatchlist(req, res, dependencies, config) {
239
+ const { db, logger } = dependencies;
240
+ const { userCid } = req.query;
241
+ const { id } = req.params;
242
+ const { alertTypes, thresholds } = req.body;
243
+
244
+ if (!userCid || !id) {
245
+ return res.status(400).json({ error: "Missing userCid or watchlist id" });
246
+ }
247
+
248
+ try {
249
+ // Get watchlist
250
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
251
+ const watchlistRef = db.collection(watchlistsCollection)
252
+ .doc(String(userCid))
253
+ .collection('lists')
254
+ .doc(id);
255
+
256
+ const watchlistDoc = await watchlistRef.get();
257
+
258
+ if (!watchlistDoc.exists) {
259
+ return res.status(404).json({ error: "Watchlist not found" });
260
+ }
261
+
262
+ const watchlistData = watchlistDoc.data();
263
+
264
+ // Default alert types
265
+ const defaultAlertTypes = alertTypes || {
266
+ newPositions: true,
267
+ volatilityChanges: true,
268
+ increasedRisk: true,
269
+ newSector: true,
270
+ increasedPositionSize: true,
271
+ newSocialPost: true
272
+ };
273
+
274
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
275
+ const subscriptionsRef = db.collection(subscriptionsCollection)
276
+ .doc(String(userCid))
277
+ .collection('alerts');
278
+
279
+ let subscribedCount = 0;
280
+
281
+ if (watchlistData.type === 'static') {
282
+ // Subscribe to all PIs in static watchlist
283
+ const items = watchlistData.items || [];
284
+
285
+ for (const item of items) {
286
+ const subscriptionData = {
287
+ userCid: Number(userCid),
288
+ piCid: item.cid,
289
+ watchlistId: id,
290
+ alertTypes: item.alertConfig || defaultAlertTypes,
291
+ thresholds: thresholds || {},
292
+ subscribedAt: FieldValue.serverTimestamp(),
293
+ lastAlertAt: null
294
+ };
295
+
296
+ await subscriptionsRef.doc(String(item.cid)).set(subscriptionData, { merge: true });
297
+ subscribedCount++;
298
+ }
299
+ } else if (watchlistData.type === 'dynamic') {
300
+ // For dynamic watchlists, we'd need to fetch the current computation result
301
+ // For now, we'll just set up the subscription structure
302
+ // The actual PIs will be determined when the computation runs
303
+ logger.log('INFO', `[subscribeToWatchlist] Dynamic watchlist subscription setup for ${id} (will be populated by computation)`);
304
+ }
305
+
306
+ logger.log('SUCCESS', `[subscribeToWatchlist] Subscribed user ${userCid} to ${subscribedCount} PIs in watchlist ${id}`);
307
+
308
+ return res.status(200).json({
309
+ success: true,
310
+ subscribed: subscribedCount,
311
+ watchlistId: id,
312
+ watchlistType: watchlistData.type
313
+ });
314
+
315
+ } catch (error) {
316
+ logger.log('ERROR', `[subscribeToWatchlist] Error subscribing to watchlist ${id} for user ${userCid}`, error);
317
+ return res.status(500).json({ error: error.message });
318
+ }
319
+ }
320
+
321
+ module.exports = {
322
+ subscribeToAlerts,
323
+ updateSubscription,
324
+ unsubscribeFromAlerts,
325
+ getUserSubscriptions,
326
+ subscribeToWatchlist
327
+ };