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,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
+