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.
- package/functions/old-generic-api/admin-api/index.js +895 -0
- package/functions/old-generic-api/helpers/api_helpers.js +457 -0
- package/functions/old-generic-api/index.js +204 -0
- package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +355 -0
- package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
- package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +212 -0
- package/functions/old-generic-api/user-api/helpers/collection_helpers.js +193 -0
- package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +68 -0
- package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +256 -0
- package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +640 -0
- package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +195 -0
- package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +503 -0
- package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
- package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +245 -0
- package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +174 -0
- package/functions/old-generic-api/user-api/helpers/data_helpers.js +87 -0
- package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +336 -0
- package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +615 -0
- package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +231 -0
- package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +641 -0
- package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +182 -0
- package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +137 -0
- package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +190 -0
- package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +66 -0
- package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +550 -0
- package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +378 -0
- package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +295 -0
- package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +162 -0
- package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +677 -0
- package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +323 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +96 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +141 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +310 -0
- package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +829 -0
- package/functions/old-generic-api/user-api/index.js +109 -0
- 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
|
+
};
|