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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Root Data Aggregation Helpers
|
|
3
|
+
* Handles writing to global rootdata collections for computation system
|
|
4
|
+
* These collections aggregate data from user-centric locations into date-based global collections
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Update PI Ratings rootdata collection
|
|
11
|
+
* Aggregates ratings from PopularInvestors/{piCid}/reviews/{reviewId} into PIRatingsData/{date}
|
|
12
|
+
*/
|
|
13
|
+
async function updateRatingsRootData(db, logger, piCid, userCid, rating, date) {
|
|
14
|
+
try {
|
|
15
|
+
const ratingsRef = db.collection('PIRatingsData').doc(date);
|
|
16
|
+
const ratingsDoc = await ratingsRef.get();
|
|
17
|
+
|
|
18
|
+
const existingData = ratingsDoc.exists ? ratingsDoc.data() : {};
|
|
19
|
+
const piCidStr = String(piCid);
|
|
20
|
+
const userCidStr = String(userCid);
|
|
21
|
+
|
|
22
|
+
// Get existing ratings for this PI or initialize
|
|
23
|
+
const piRatings = existingData[piCidStr] || {
|
|
24
|
+
totalRatings: 0,
|
|
25
|
+
ratingsByUser: {},
|
|
26
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Update rating for this user
|
|
30
|
+
const oldRating = piRatings.ratingsByUser[userCidStr];
|
|
31
|
+
piRatings.ratingsByUser[userCidStr] = rating;
|
|
32
|
+
|
|
33
|
+
// Recalculate average rating
|
|
34
|
+
const ratings = Object.values(piRatings.ratingsByUser);
|
|
35
|
+
const totalRatings = ratings.length;
|
|
36
|
+
const sumRatings = ratings.reduce((sum, r) => sum + r, 0);
|
|
37
|
+
const averageRating = totalRatings > 0 ? sumRatings / totalRatings : 0;
|
|
38
|
+
|
|
39
|
+
// Update the data
|
|
40
|
+
const updateData = {
|
|
41
|
+
[piCidStr]: {
|
|
42
|
+
averageRating: Number(averageRating.toFixed(2)),
|
|
43
|
+
totalRatings: totalRatings,
|
|
44
|
+
ratingsByUser: piRatings.ratingsByUser,
|
|
45
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
46
|
+
},
|
|
47
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await ratingsRef.set(updateData, { merge: true });
|
|
51
|
+
|
|
52
|
+
logger.log('INFO', `[updateRatingsRootData] Updated ratings for PI ${piCid} on ${date}`);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.log('ERROR', `[updateRatingsRootData] Error updating ratings rootdata for PI ${piCid}:`, error);
|
|
55
|
+
// Don't throw - this is a non-critical aggregation
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update PI Page Views rootdata collection
|
|
61
|
+
* Aggregates page views from PopularInvestors/{piCid}/profileViews/{date} into PIPageViewsData/{date}
|
|
62
|
+
*/
|
|
63
|
+
async function updatePageViewsRootData(db, logger, piCid, userCid, date) {
|
|
64
|
+
try {
|
|
65
|
+
const pageViewsRef = db.collection('PIPageViewsData').doc(date);
|
|
66
|
+
const pageViewsDoc = await pageViewsRef.get();
|
|
67
|
+
|
|
68
|
+
const existingData = pageViewsDoc.exists ? pageViewsDoc.data() : {};
|
|
69
|
+
const piCidStr = String(piCid);
|
|
70
|
+
const userCidStr = String(userCid);
|
|
71
|
+
|
|
72
|
+
// Get existing page views for this PI or initialize
|
|
73
|
+
const piViews = existingData[piCidStr] || {
|
|
74
|
+
totalViews: 0,
|
|
75
|
+
uniqueViewers: 0,
|
|
76
|
+
viewsByUser: {},
|
|
77
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Increment total views
|
|
81
|
+
piViews.totalViews = (piViews.totalViews || 0) + 1;
|
|
82
|
+
|
|
83
|
+
// Update or add user view
|
|
84
|
+
if (!piViews.viewsByUser[userCidStr]) {
|
|
85
|
+
piViews.uniqueViewers = (piViews.uniqueViewers || 0) + 1;
|
|
86
|
+
piViews.viewsByUser[userCidStr] = {
|
|
87
|
+
viewCount: 0,
|
|
88
|
+
lastViewed: FieldValue.serverTimestamp()
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
piViews.viewsByUser[userCidStr].viewCount = (piViews.viewsByUser[userCidStr].viewCount || 0) + 1;
|
|
93
|
+
piViews.viewsByUser[userCidStr].lastViewed = FieldValue.serverTimestamp();
|
|
94
|
+
|
|
95
|
+
// Update the data
|
|
96
|
+
const updateData = {
|
|
97
|
+
[piCidStr]: {
|
|
98
|
+
totalViews: piViews.totalViews,
|
|
99
|
+
uniqueViewers: piViews.uniqueViewers,
|
|
100
|
+
viewsByUser: piViews.viewsByUser,
|
|
101
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
102
|
+
},
|
|
103
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await pageViewsRef.set(updateData, { merge: true });
|
|
107
|
+
|
|
108
|
+
logger.log('INFO', `[updatePageViewsRootData] Updated page views for PI ${piCid} on ${date}`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.log('ERROR', `[updatePageViewsRootData] Error updating page views rootdata for PI ${piCid}:`, error);
|
|
111
|
+
// Don't throw - this is a non-critical aggregation
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update Watchlist Membership rootdata collection
|
|
117
|
+
* Aggregates watchlist membership from SignedInUsers/{cid}/watchlists into WatchlistMembershipData/{date}
|
|
118
|
+
* This should be called when a PI is added/removed from a watchlist
|
|
119
|
+
* This provides a single point of reference for all watchlists made in that day
|
|
120
|
+
*/
|
|
121
|
+
async function updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, date, action = 'add') {
|
|
122
|
+
try {
|
|
123
|
+
const membershipRef = db.collection('WatchlistMembershipData').doc(date);
|
|
124
|
+
const membershipDoc = await membershipRef.get();
|
|
125
|
+
|
|
126
|
+
const existingData = membershipDoc.exists ? membershipDoc.data() : {};
|
|
127
|
+
const piCidStr = String(piCid);
|
|
128
|
+
const userCidStr = String(userCid);
|
|
129
|
+
|
|
130
|
+
// Get existing membership for this PI or initialize
|
|
131
|
+
const piMembership = existingData[piCidStr] || {
|
|
132
|
+
totalUsers: 0,
|
|
133
|
+
users: [],
|
|
134
|
+
publicWatchlistCount: 0,
|
|
135
|
+
privateWatchlistCount: 0,
|
|
136
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (action === 'add') {
|
|
140
|
+
// Add user if not already in list
|
|
141
|
+
if (!piMembership.users.includes(userCidStr)) {
|
|
142
|
+
piMembership.users.push(userCidStr);
|
|
143
|
+
piMembership.totalUsers = piMembership.users.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Update counts
|
|
147
|
+
if (isPublic) {
|
|
148
|
+
piMembership.publicWatchlistCount = (piMembership.publicWatchlistCount || 0) + 1;
|
|
149
|
+
} else {
|
|
150
|
+
piMembership.privateWatchlistCount = (piMembership.privateWatchlistCount || 0) + 1;
|
|
151
|
+
}
|
|
152
|
+
} else if (action === 'remove') {
|
|
153
|
+
// Remove user from list
|
|
154
|
+
piMembership.users = piMembership.users.filter(u => u !== userCidStr);
|
|
155
|
+
piMembership.totalUsers = piMembership.users.length;
|
|
156
|
+
|
|
157
|
+
// Update counts (decrement if > 0)
|
|
158
|
+
if (isPublic && piMembership.publicWatchlistCount > 0) {
|
|
159
|
+
piMembership.publicWatchlistCount = piMembership.publicWatchlistCount - 1;
|
|
160
|
+
} else if (!isPublic && piMembership.privateWatchlistCount > 0) {
|
|
161
|
+
piMembership.privateWatchlistCount = piMembership.privateWatchlistCount - 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Update the data - ensure date field is set for rootdata indexer
|
|
166
|
+
const updateData = {
|
|
167
|
+
date: date, // Ensure date field exists for rootdata indexer
|
|
168
|
+
[piCidStr]: {
|
|
169
|
+
totalUsers: piMembership.totalUsers,
|
|
170
|
+
users: piMembership.users,
|
|
171
|
+
publicWatchlistCount: piMembership.publicWatchlistCount,
|
|
172
|
+
privateWatchlistCount: piMembership.privateWatchlistCount,
|
|
173
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
174
|
+
},
|
|
175
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
await membershipRef.set(updateData, { merge: true });
|
|
179
|
+
|
|
180
|
+
logger.log('INFO', `[updateWatchlistMembershipRootData] Updated watchlist membership for PI ${piCid} on ${date} (action: ${action})`);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logger.log('ERROR', `[updateWatchlistMembershipRootData] Error updating watchlist membership rootdata for PI ${piCid}:`, error);
|
|
183
|
+
// Don't throw - this is a non-critical aggregation
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Update Popular Investor watchlist data
|
|
189
|
+
* Tracks watchlist additions per PI in PopularInvestors/{piCid}/watchlistData
|
|
190
|
+
* This provides a time series of watchlist additions over time per popular investor
|
|
191
|
+
*/
|
|
192
|
+
async function updatePIWatchlistData(db, logger, piCid, userCid, date, action = 'add') {
|
|
193
|
+
try {
|
|
194
|
+
const piCidStr = String(piCid);
|
|
195
|
+
const userCidStr = String(userCid);
|
|
196
|
+
const piWatchlistRef = db.collection('PopularInvestors').doc(piCidStr).collection('watchlistData').doc('current');
|
|
197
|
+
|
|
198
|
+
const piWatchlistDoc = await piWatchlistRef.get();
|
|
199
|
+
const existingData = piWatchlistDoc.exists ? piWatchlistDoc.data() : {};
|
|
200
|
+
|
|
201
|
+
// Initialize structure if needed
|
|
202
|
+
const watchlistData = existingData || {
|
|
203
|
+
totalUsers: 0,
|
|
204
|
+
userCids: [],
|
|
205
|
+
dailyAdditions: {},
|
|
206
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (action === 'add') {
|
|
210
|
+
// Add user CID if not already present
|
|
211
|
+
if (!watchlistData.userCids || !Array.isArray(watchlistData.userCids)) {
|
|
212
|
+
watchlistData.userCids = [];
|
|
213
|
+
}
|
|
214
|
+
if (!watchlistData.userCids.includes(userCidStr)) {
|
|
215
|
+
watchlistData.userCids.push(userCidStr);
|
|
216
|
+
watchlistData.totalUsers = watchlistData.userCids.length;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Track daily additions with timestamp
|
|
220
|
+
if (!watchlistData.dailyAdditions || typeof watchlistData.dailyAdditions !== 'object') {
|
|
221
|
+
watchlistData.dailyAdditions = {};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Initialize date entry if needed
|
|
225
|
+
if (!watchlistData.dailyAdditions[date]) {
|
|
226
|
+
watchlistData.dailyAdditions[date] = {
|
|
227
|
+
count: 0,
|
|
228
|
+
userCids: [],
|
|
229
|
+
timestamp: FieldValue.serverTimestamp()
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add user to this date's additions if not already present
|
|
234
|
+
const dateEntry = watchlistData.dailyAdditions[date];
|
|
235
|
+
if (!dateEntry.userCids.includes(userCidStr)) {
|
|
236
|
+
dateEntry.userCids.push(userCidStr);
|
|
237
|
+
dateEntry.count = dateEntry.userCids.length;
|
|
238
|
+
dateEntry.timestamp = FieldValue.serverTimestamp();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
} else if (action === 'remove') {
|
|
242
|
+
// Remove user CID
|
|
243
|
+
if (watchlistData.userCids && Array.isArray(watchlistData.userCids)) {
|
|
244
|
+
watchlistData.userCids = watchlistData.userCids.filter(cid => cid !== userCidStr);
|
|
245
|
+
watchlistData.totalUsers = watchlistData.userCids.length;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Update daily additions for this date
|
|
249
|
+
if (watchlistData.dailyAdditions && watchlistData.dailyAdditions[date]) {
|
|
250
|
+
const dateEntry = watchlistData.dailyAdditions[date];
|
|
251
|
+
if (dateEntry.userCids && Array.isArray(dateEntry.userCids)) {
|
|
252
|
+
dateEntry.userCids = dateEntry.userCids.filter(cid => cid !== userCidStr);
|
|
253
|
+
dateEntry.count = dateEntry.userCids.length;
|
|
254
|
+
dateEntry.timestamp = FieldValue.serverTimestamp();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Update the document
|
|
260
|
+
watchlistData.lastUpdated = FieldValue.serverTimestamp();
|
|
261
|
+
await piWatchlistRef.set(watchlistData, { merge: true });
|
|
262
|
+
|
|
263
|
+
logger.log('INFO', `[updatePIWatchlistData] Updated watchlist data for PI ${piCid} (action: ${action}, user: ${userCid})`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.log('ERROR', `[updatePIWatchlistData] Error updating PI watchlist data for PI ${piCid}:`, error);
|
|
266
|
+
// Don't throw - this is a non-critical aggregation
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update PI Alert History rootdata collection
|
|
272
|
+
* Records alert triggers in PIAlertHistoryData/{date}
|
|
273
|
+
*/
|
|
274
|
+
async function updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids = []) {
|
|
275
|
+
try {
|
|
276
|
+
const alertHistoryRef = db.collection('PIAlertHistoryData').doc(computationDate);
|
|
277
|
+
const alertHistoryDoc = await alertHistoryRef.get();
|
|
278
|
+
|
|
279
|
+
const existingData = alertHistoryDoc.exists ? alertHistoryDoc.data() : {};
|
|
280
|
+
const piCidStr = String(piCid);
|
|
281
|
+
const alertTypeStr = String(alertType.computationName || alertType.id || alertType);
|
|
282
|
+
|
|
283
|
+
// Get existing alert history for this PI or initialize
|
|
284
|
+
const piAlerts = existingData[piCidStr] || {};
|
|
285
|
+
|
|
286
|
+
// Update or initialize alert type data
|
|
287
|
+
const alertTypeData = piAlerts[alertTypeStr] || {
|
|
288
|
+
triggered: false,
|
|
289
|
+
count: 0,
|
|
290
|
+
triggeredFor: [],
|
|
291
|
+
metadata: {},
|
|
292
|
+
lastTriggered: null
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Update alert data
|
|
296
|
+
alertTypeData.triggered = true;
|
|
297
|
+
alertTypeData.count = (alertTypeData.count || 0) + 1;
|
|
298
|
+
alertTypeData.triggeredFor = [...new Set([...alertTypeData.triggeredFor, ...triggeredUserCids.map(c => String(c))])];
|
|
299
|
+
alertTypeData.metadata = computationMetadata || {};
|
|
300
|
+
alertTypeData.lastTriggered = FieldValue.serverTimestamp();
|
|
301
|
+
|
|
302
|
+
// Update the data
|
|
303
|
+
const updateData = {
|
|
304
|
+
[piCidStr]: {
|
|
305
|
+
[alertTypeStr]: alertTypeData,
|
|
306
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
307
|
+
},
|
|
308
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
await alertHistoryRef.set(updateData, { merge: true });
|
|
312
|
+
|
|
313
|
+
logger.log('INFO', `[updateAlertHistoryRootData] Updated alert history for PI ${piCid}, alert type ${alertTypeStr} on ${computationDate}`);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
logger.log('ERROR', `[updateAlertHistoryRootData] Error updating alert history rootdata for PI ${piCid}:`, error);
|
|
316
|
+
// Don't throw - this is a non-critical aggregation
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Record "all clear" in alert history (when alert computation runs but doesn't trigger)
|
|
322
|
+
*/
|
|
323
|
+
async function updateAllClearAlertHistory(db, logger, piCid, alertType, computationDate) {
|
|
324
|
+
try {
|
|
325
|
+
const alertHistoryRef = db.collection('PIAlertHistoryData').doc(computationDate);
|
|
326
|
+
const alertHistoryDoc = await alertHistoryRef.get();
|
|
327
|
+
|
|
328
|
+
const existingData = alertHistoryDoc.exists ? alertHistoryDoc.data() : {};
|
|
329
|
+
const piCidStr = String(piCid);
|
|
330
|
+
const alertTypeStr = String(alertType.computationName || alertType.id || alertType);
|
|
331
|
+
|
|
332
|
+
// Get existing alert history for this PI or initialize
|
|
333
|
+
const piAlerts = existingData[piCidStr] || {};
|
|
334
|
+
|
|
335
|
+
// Initialize alert type data if it doesn't exist
|
|
336
|
+
if (!piAlerts[alertTypeStr]) {
|
|
337
|
+
piAlerts[alertTypeStr] = {
|
|
338
|
+
triggered: false,
|
|
339
|
+
count: 0,
|
|
340
|
+
triggeredFor: [],
|
|
341
|
+
metadata: {},
|
|
342
|
+
lastTriggered: null
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Mark as not triggered (all clear)
|
|
347
|
+
const alertTypeData = piAlerts[alertTypeStr];
|
|
348
|
+
alertTypeData.triggered = false;
|
|
349
|
+
alertTypeData.metadata = { allClear: true };
|
|
350
|
+
alertTypeData.lastChecked = FieldValue.serverTimestamp();
|
|
351
|
+
|
|
352
|
+
// Update the data
|
|
353
|
+
const updateData = {
|
|
354
|
+
[piCidStr]: {
|
|
355
|
+
[alertTypeStr]: alertTypeData,
|
|
356
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
357
|
+
},
|
|
358
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
await alertHistoryRef.set(updateData, { merge: true });
|
|
362
|
+
|
|
363
|
+
logger.log('INFO', `[updateAllClearAlertHistory] Recorded all clear for PI ${piCid}, alert type ${alertTypeStr} on ${computationDate}`);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
logger.log('ERROR', `[updateAllClearAlertHistory] Error updating all clear alert history for PI ${piCid}:`, error);
|
|
366
|
+
// Don't throw - this is a non-critical aggregation
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = {
|
|
371
|
+
updateRatingsRootData,
|
|
372
|
+
updatePageViewsRootData,
|
|
373
|
+
updateWatchlistMembershipRootData,
|
|
374
|
+
updatePIWatchlistData,
|
|
375
|
+
updateAlertHistoryRootData,
|
|
376
|
+
updateAllClearAlertHistory
|
|
377
|
+
};
|
|
378
|
+
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investor Request Helpers
|
|
3
|
+
* Handles PI addition requests and rankings checks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
+
const { findLatestRankingsDate } = require('../core/data_lookup_helpers');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /user/requests/pi-addition
|
|
11
|
+
* Request to add a Popular Investor to the database
|
|
12
|
+
*/
|
|
13
|
+
async function requestPiAddition(req, res, dependencies, config) {
|
|
14
|
+
const { db, logger } = dependencies;
|
|
15
|
+
const { userCid, username, piUsername, piCid } = req.body;
|
|
16
|
+
|
|
17
|
+
if (!userCid || !username || !piUsername) {
|
|
18
|
+
return res.status(400).json({ error: "Missing required fields: userCid, username, piUsername" });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const requestsCollection = config.requestsCollection || 'requests';
|
|
23
|
+
const requestId = `pi_add_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
24
|
+
|
|
25
|
+
const requestData = {
|
|
26
|
+
id: requestId,
|
|
27
|
+
type: 'popular_investor_addition',
|
|
28
|
+
requestedBy: {
|
|
29
|
+
userCid: Number(userCid),
|
|
30
|
+
username: username
|
|
31
|
+
},
|
|
32
|
+
popularInvestor: {
|
|
33
|
+
username: piUsername,
|
|
34
|
+
cid: piCid || null
|
|
35
|
+
},
|
|
36
|
+
status: 'pending',
|
|
37
|
+
requestedAt: FieldValue.serverTimestamp(),
|
|
38
|
+
processedAt: null,
|
|
39
|
+
notes: null
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const requestRef = db.collection(requestsCollection)
|
|
43
|
+
.doc('popular_investor_copy_additions')
|
|
44
|
+
.collection('requests')
|
|
45
|
+
.doc(requestId);
|
|
46
|
+
|
|
47
|
+
// Write to global collection
|
|
48
|
+
await requestRef.set(requestData);
|
|
49
|
+
|
|
50
|
+
// Also write to user-centric collection (dual-write)
|
|
51
|
+
const { writeWithMigration } = require('../core/path_resolution_helpers');
|
|
52
|
+
try {
|
|
53
|
+
await writeWithMigration(
|
|
54
|
+
db,
|
|
55
|
+
'signedInUsers',
|
|
56
|
+
'piAdditionRequests',
|
|
57
|
+
{ cid: userCid },
|
|
58
|
+
requestData,
|
|
59
|
+
{
|
|
60
|
+
isCollection: true,
|
|
61
|
+
merge: false,
|
|
62
|
+
dataType: 'piAdditionRequests',
|
|
63
|
+
config,
|
|
64
|
+
documentId: requestId,
|
|
65
|
+
dualWrite: false, // Don't dual-write to legacy (new feature)
|
|
66
|
+
collectionRegistry: dependencies.collectionRegistry
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
} catch (userWriteErr) {
|
|
70
|
+
// Log but don't fail - global write succeeded
|
|
71
|
+
logger.log('WARN', `[requestPiAddition] Failed to write to user-centric collection: ${userWriteErr.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.log('SUCCESS', `[requestPiAddition] User ${userCid} requested addition of PI ${piUsername} (CID: ${piCid || 'unknown'})`);
|
|
75
|
+
|
|
76
|
+
return res.status(201).json({
|
|
77
|
+
success: true,
|
|
78
|
+
requestId: requestId,
|
|
79
|
+
message: "Request submitted successfully"
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.log('ERROR', `[requestPiAddition] Error submitting request`, error);
|
|
84
|
+
return res.status(500).json({ error: error.message });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* GET /user/me/watchlists/:id/rankings-check
|
|
90
|
+
* Check which PIs in a watchlist are in the master list (single source of truth)
|
|
91
|
+
* UPDATED: Now checks against the master list instead of latest rankings date
|
|
92
|
+
*/
|
|
93
|
+
async function checkPisInRankings(req, res, dependencies, config) {
|
|
94
|
+
const { db, logger, collectionRegistry } = dependencies;
|
|
95
|
+
const { userCid } = req.query;
|
|
96
|
+
const { id } = req.params;
|
|
97
|
+
|
|
98
|
+
if (!userCid || !id) {
|
|
99
|
+
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Read watchlist from new path with migration
|
|
104
|
+
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
105
|
+
const watchlistResult = await readWithMigration(
|
|
106
|
+
db,
|
|
107
|
+
'signedInUsers',
|
|
108
|
+
'watchlists',
|
|
109
|
+
{ cid: userCid },
|
|
110
|
+
{
|
|
111
|
+
isCollection: false,
|
|
112
|
+
dataType: 'watchlists',
|
|
113
|
+
config,
|
|
114
|
+
logger,
|
|
115
|
+
documentId: id,
|
|
116
|
+
collectionRegistry: dependencies.collectionRegistry
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
let items = [];
|
|
121
|
+
if (watchlistResult && watchlistResult.data) {
|
|
122
|
+
items = watchlistResult.data.items || [];
|
|
123
|
+
} else {
|
|
124
|
+
// Fallback to legacy path
|
|
125
|
+
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
126
|
+
const watchlistRef = db.collection(watchlistsCollection)
|
|
127
|
+
.doc(String(userCid))
|
|
128
|
+
.collection('lists')
|
|
129
|
+
.doc(id);
|
|
130
|
+
|
|
131
|
+
const watchlistDoc = await watchlistRef.get();
|
|
132
|
+
|
|
133
|
+
if (!watchlistDoc.exists) {
|
|
134
|
+
return res.status(404).json({ error: "Watchlist not found" });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const watchlistData = watchlistDoc.data();
|
|
138
|
+
items = watchlistData.items || [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get the master list path from collection registry
|
|
142
|
+
let masterListPath = 'system_state/popular_investor_master_list';
|
|
143
|
+
|
|
144
|
+
if (collectionRegistry && collectionRegistry.getCollectionPath) {
|
|
145
|
+
try {
|
|
146
|
+
masterListPath = collectionRegistry.getCollectionPath('system', 'popularInvestorMasterList', {});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger.log('WARN', `[checkPisInRankings] Failed to get master list path from registry, using default: ${err.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Fetch master list document
|
|
153
|
+
const masterListRef = db.doc(masterListPath);
|
|
154
|
+
const masterListDoc = await masterListRef.get();
|
|
155
|
+
|
|
156
|
+
if (!masterListDoc.exists) {
|
|
157
|
+
// Master list doesn't exist yet - fallback to legacy rankings check
|
|
158
|
+
logger.log('WARN', `[checkPisInRankings] Master list not found, falling back to legacy rankings check`);
|
|
159
|
+
const legacyResult = await checkPisInRankingsLegacy(db, items, config, logger);
|
|
160
|
+
return res.status(200).json(legacyResult);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const masterListData = masterListDoc.data();
|
|
164
|
+
const investors = masterListData.investors || {};
|
|
165
|
+
|
|
166
|
+
// Create sets for quick lookup: by CID and by username (case-insensitive)
|
|
167
|
+
const investorsByCid = new Set();
|
|
168
|
+
const investorsByUsername = new Set();
|
|
169
|
+
|
|
170
|
+
for (const [cid, piData] of Object.entries(investors)) {
|
|
171
|
+
if (cid) {
|
|
172
|
+
investorsByCid.add(String(cid));
|
|
173
|
+
}
|
|
174
|
+
if (piData.username) {
|
|
175
|
+
investorsByUsername.add(piData.username.toLowerCase().trim());
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check which watchlist PIs are NOT in the master list
|
|
180
|
+
// Check by both CID and username to be thorough
|
|
181
|
+
const notInRankings = [];
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
const cidStr = String(item.cid);
|
|
184
|
+
const username = item.username ? item.username.toLowerCase().trim() : null;
|
|
185
|
+
|
|
186
|
+
// Check if PI exists in master list by CID or username
|
|
187
|
+
const existsByCid = investorsByCid.has(cidStr);
|
|
188
|
+
const existsByUsername = username && investorsByUsername.has(username);
|
|
189
|
+
|
|
190
|
+
if (!existsByCid && !existsByUsername) {
|
|
191
|
+
notInRankings.push(item.cid);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return res.status(200).json({
|
|
196
|
+
notInRankings,
|
|
197
|
+
rankingsDate: null, // No longer using rankings date
|
|
198
|
+
totalChecked: items.length,
|
|
199
|
+
inRankings: items.length - notInRankings.length,
|
|
200
|
+
usingMasterList: true
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.log('ERROR', `[checkPisInRankings] Error checking PIs in master list`, error);
|
|
205
|
+
return res.status(500).json({ error: error.message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Legacy method: Check against latest rankings date
|
|
211
|
+
* Used as fallback when master list is not available
|
|
212
|
+
*/
|
|
213
|
+
async function checkPisInRankingsLegacy(db, items, config, logger) {
|
|
214
|
+
try {
|
|
215
|
+
// Find latest available rankings date
|
|
216
|
+
const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
217
|
+
const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
|
|
218
|
+
|
|
219
|
+
if (!rankingsDate) {
|
|
220
|
+
const notInRankings = items.map(item => item.cid);
|
|
221
|
+
return {
|
|
222
|
+
notInRankings,
|
|
223
|
+
rankingsDate: null,
|
|
224
|
+
totalChecked: items.length,
|
|
225
|
+
inRankings: 0,
|
|
226
|
+
usingMasterList: false,
|
|
227
|
+
message: "No rankings data available"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fetch rankings data
|
|
232
|
+
const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
|
|
233
|
+
const rankingsDoc = await rankingsRef.get();
|
|
234
|
+
|
|
235
|
+
if (!rankingsDoc.exists) {
|
|
236
|
+
const notInRankings = items.map(item => item.cid);
|
|
237
|
+
return {
|
|
238
|
+
notInRankings,
|
|
239
|
+
rankingsDate: null,
|
|
240
|
+
totalChecked: items.length,
|
|
241
|
+
inRankings: 0,
|
|
242
|
+
usingMasterList: false,
|
|
243
|
+
message: "Rankings document not found"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const rawRankingsData = rankingsDoc.data();
|
|
248
|
+
// Decompress if needed
|
|
249
|
+
const { tryDecompress } = require('../core/compression_helpers');
|
|
250
|
+
const rankingsData = tryDecompress(rawRankingsData);
|
|
251
|
+
const rankingsItems = rankingsData.Items || [];
|
|
252
|
+
|
|
253
|
+
// Create a set of CIDs that exist in rankings
|
|
254
|
+
const rankingsCIDs = new Set();
|
|
255
|
+
for (const item of rankingsItems) {
|
|
256
|
+
if (item.CustomerId) {
|
|
257
|
+
rankingsCIDs.add(String(item.CustomerId));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check which watchlist PIs are NOT in rankings
|
|
262
|
+
const notInRankings = [];
|
|
263
|
+
for (const item of items) {
|
|
264
|
+
const cidStr = String(item.cid);
|
|
265
|
+
if (!rankingsCIDs.has(cidStr)) {
|
|
266
|
+
notInRankings.push(item.cid);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
notInRankings,
|
|
272
|
+
rankingsDate: rankingsDate,
|
|
273
|
+
totalChecked: items.length,
|
|
274
|
+
inRankings: items.length - notInRankings.length,
|
|
275
|
+
usingMasterList: false
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
logger.log('ERROR', `[checkPisInRankingsLegacy] Error in legacy rankings check`, error);
|
|
279
|
+
// Return all as not in rankings on error
|
|
280
|
+
return {
|
|
281
|
+
notInRankings: items.map(item => item.cid),
|
|
282
|
+
rankingsDate: null,
|
|
283
|
+
totalChecked: items.length,
|
|
284
|
+
inRankings: 0,
|
|
285
|
+
usingMasterList: false,
|
|
286
|
+
message: error.message
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
requestPiAddition,
|
|
293
|
+
checkPisInRankings
|
|
294
|
+
};
|
|
295
|
+
|