bulltrackers-module 1.0.504 → 1.0.506
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/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
- package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
- package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
- package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
- package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
- package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
- package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
- package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
- package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
- package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
- package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
- package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
- package/functions/generic-api/user-api/index.js +9 -9
- package/functions/task-engine/handler_creator.js +7 -6
- package/package.json +1 -1
- package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
- package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
- package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
- package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
- /package/functions/generic-api/user-api/helpers/{alert_helpers.js → alerts/alert_helpers.js} +0 -0
package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js}
RENAMED
|
@@ -81,7 +81,7 @@ async function hasUserCopied(db, userCid, piCid, config) {
|
|
|
81
81
|
|
|
82
82
|
try {
|
|
83
83
|
// === DEV OVERRIDE CHECK (for developer accounts) ===
|
|
84
|
-
const { hasUserCopiedWithDevOverride } = require('
|
|
84
|
+
const { hasUserCopiedWithDevOverride } = require('../dev/dev_helpers');
|
|
85
85
|
const devResult = await hasUserCopiedWithDevOverride(db, userCid, piCid, config, console);
|
|
86
86
|
if (devResult !== null) {
|
|
87
87
|
// devResult is true/false (not null), meaning dev override is active
|
|
@@ -143,7 +143,7 @@ async function hasUserCopied(db, userCid, piCid, config) {
|
|
|
143
143
|
// If key exists, merge arrays or objects
|
|
144
144
|
if (Array.isArray(mergedData[key]) && Array.isArray(value)) {
|
|
145
145
|
mergedData[key] = [...mergedData[key], ...value];
|
|
146
|
-
} else if (typeof mergedData[key] === 'object' && typeof value === 'object'
|
|
146
|
+
} else if (typeof mergedData[key] === 'object' && typeof value === 'object') {
|
|
147
147
|
mergedData[key] = { ...mergedData[key], ...value };
|
|
148
148
|
} else {
|
|
149
149
|
mergedData[key] = value;
|
|
@@ -156,7 +156,7 @@ async function hasUserCopied(db, userCid, piCid, config) {
|
|
|
156
156
|
}
|
|
157
157
|
} else {
|
|
158
158
|
// Data is in main document - decompress if needed
|
|
159
|
-
const { tryDecompress } = require('
|
|
159
|
+
const { tryDecompress } = require('../data_helpers');
|
|
160
160
|
mergedData = tryDecompress(rawData);
|
|
161
161
|
|
|
162
162
|
// Handle string decompression result
|
|
@@ -275,7 +275,7 @@ async function submitReview(req, res, dependencies, config) {
|
|
|
275
275
|
|
|
276
276
|
try {
|
|
277
277
|
// Check for dev override impersonation
|
|
278
|
-
const { getEffectiveCid, getDevOverride } = require('
|
|
278
|
+
const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
|
|
279
279
|
const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
|
|
280
280
|
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
281
281
|
const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
|
|
@@ -289,7 +289,7 @@ async function submitReview(req, res, dependencies, config) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// Check if the effective user is a Popular Investor
|
|
292
|
-
const { checkIfUserIsPI } = require('
|
|
292
|
+
const { checkIfUserIsPI } = require('../data_helpers');
|
|
293
293
|
const rankEntry = await checkIfUserIsPI(db, effectiveCid, config, logger);
|
|
294
294
|
|
|
295
295
|
// Block Popular Investors from reviewing other Popular Investors
|
|
@@ -321,33 +321,8 @@ async function submitReview(req, res, dependencies, config) {
|
|
|
321
321
|
|
|
322
322
|
// 3. Check if review already exists (for update) - use effective CID for review ID
|
|
323
323
|
const reviewId = `${effectiveCid}_${piCid}`;
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
const { collectionRegistry } = dependencies;
|
|
327
|
-
|
|
328
|
-
// Check existing review in new or legacy structure
|
|
329
|
-
let existingReview = null;
|
|
330
|
-
let isUpdate = false;
|
|
331
|
-
|
|
332
|
-
if (collectionRegistry) {
|
|
333
|
-
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
334
|
-
piCid: String(piCid)
|
|
335
|
-
}) + `/${reviewId}`;
|
|
336
|
-
|
|
337
|
-
const legacyPath = `${reviewsCollection}/${reviewId}`;
|
|
338
|
-
|
|
339
|
-
const result = await readWithFallback(db, newPath, legacyPath);
|
|
340
|
-
if (result && result.data) {
|
|
341
|
-
existingReview = { exists: true, data: () => result.data };
|
|
342
|
-
isUpdate = true;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Fallback to legacy check
|
|
347
|
-
if (!existingReview) {
|
|
348
|
-
existingReview = await db.collection(reviewsCollection).doc(reviewId).get();
|
|
349
|
-
isUpdate = existingReview.exists;
|
|
350
|
-
}
|
|
324
|
+
const existingReview = await db.collection(reviewsCollection).doc(reviewId).get();
|
|
325
|
+
const isUpdate = existingReview.exists;
|
|
351
326
|
|
|
352
327
|
// 4. Store/Update Review (store effective CID, but track actual CID for audit)
|
|
353
328
|
const reviewData = {
|
|
@@ -366,18 +341,7 @@ async function submitReview(req, res, dependencies, config) {
|
|
|
366
341
|
reviewData.createdAt = FieldValue.serverTimestamp();
|
|
367
342
|
}
|
|
368
343
|
|
|
369
|
-
|
|
370
|
-
if (collectionRegistry) {
|
|
371
|
-
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
372
|
-
piCid: String(piCid)
|
|
373
|
-
}) + `/${reviewId}`;
|
|
374
|
-
|
|
375
|
-
const legacyPath = `${reviewsCollection}/${reviewId}`;
|
|
376
|
-
|
|
377
|
-
await writeDual(db, newPath, legacyPath, reviewData, { isCollection: false, merge: true });
|
|
378
|
-
} else {
|
|
379
|
-
await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
|
|
380
|
-
}
|
|
344
|
+
await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
|
|
381
345
|
|
|
382
346
|
logger.log('INFO', `[Review] User ${userCid} ${isUpdate ? 'updated' : 'submitted'} review for PI ${piCid}`);
|
|
383
347
|
return res.status(200).json({
|
|
@@ -404,39 +368,13 @@ async function getReviews(req, res, dependencies, config) {
|
|
|
404
368
|
|
|
405
369
|
try {
|
|
406
370
|
const piCidNum = Number(piCid);
|
|
407
|
-
const { getCollectionPath, extractCollectionName } = require('./collection_helpers');
|
|
408
|
-
const { collectionRegistry } = dependencies;
|
|
409
371
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
piCid: String(piCidNum)
|
|
417
|
-
});
|
|
418
|
-
const collectionName = extractCollectionName(newPath);
|
|
419
|
-
|
|
420
|
-
const reviewsRef = db.collection(collectionName)
|
|
421
|
-
.doc(String(piCidNum))
|
|
422
|
-
.collection('reviews')
|
|
423
|
-
.orderBy('createdAt', 'desc')
|
|
424
|
-
.limit(100);
|
|
425
|
-
|
|
426
|
-
snapshot = await reviewsRef.get();
|
|
427
|
-
} catch (newError) {
|
|
428
|
-
logger.log('WARN', `[getReviews] New structure failed, trying legacy: ${newError.message}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Fallback to legacy structure
|
|
433
|
-
if (!snapshot || snapshot.empty) {
|
|
434
|
-
snapshot = await db.collection(reviewsCollection)
|
|
435
|
-
.where('piCid', '==', piCidNum)
|
|
436
|
-
.orderBy('createdAt', 'desc')
|
|
437
|
-
.limit(100)
|
|
438
|
-
.get();
|
|
439
|
-
}
|
|
372
|
+
// Query reviews for this PI
|
|
373
|
+
const snapshot = await db.collection(reviewsCollection)
|
|
374
|
+
.where('piCid', '==', piCidNum)
|
|
375
|
+
.orderBy('createdAt', 'desc')
|
|
376
|
+
.limit(100) // Increased limit
|
|
377
|
+
.get();
|
|
440
378
|
|
|
441
379
|
const reviews = [];
|
|
442
380
|
let totalStars = 0;
|
|
@@ -510,38 +448,16 @@ async function getUserReview(req, res, dependencies, config) {
|
|
|
510
448
|
}
|
|
511
449
|
|
|
512
450
|
try {
|
|
513
|
-
const { getCollectionPath, readWithFallback } = require('./collection_helpers');
|
|
514
|
-
const { collectionRegistry } = dependencies;
|
|
515
451
|
const reviewId = `${userCid}_${piCid}`;
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (collectionRegistry) {
|
|
521
|
-
const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
|
|
522
|
-
piCid: String(piCid)
|
|
523
|
-
}) + `/${reviewId}`;
|
|
524
|
-
|
|
525
|
-
const legacyPath = `${reviewsCollection}/${reviewId}`;
|
|
526
|
-
|
|
527
|
-
const result = await readWithFallback(db, newPath, legacyPath);
|
|
528
|
-
if (result && result.data) {
|
|
529
|
-
reviewData = result.data;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Fallback to legacy
|
|
534
|
-
if (!reviewData) {
|
|
535
|
-
const reviewDoc = await db.collection(reviewsCollection).doc(reviewId).get();
|
|
536
|
-
if (!reviewDoc.exists) {
|
|
537
|
-
return res.status(200).json({ exists: false, review: null });
|
|
538
|
-
}
|
|
539
|
-
reviewData = reviewDoc.data();
|
|
452
|
+
const reviewDoc = await db.collection(reviewsCollection).doc(reviewId).get();
|
|
453
|
+
|
|
454
|
+
if (!reviewDoc.exists) {
|
|
455
|
+
return res.status(200).json({ exists: false, review: null });
|
|
540
456
|
}
|
|
541
|
-
|
|
542
|
-
const data =
|
|
457
|
+
|
|
458
|
+
const data = reviewDoc.data();
|
|
543
459
|
const review = {
|
|
544
|
-
id:
|
|
460
|
+
id: reviewDoc.id,
|
|
545
461
|
rating: data.rating,
|
|
546
462
|
comment: data.comment || "",
|
|
547
463
|
isAnonymous: data.isAnonymous || false,
|
|
@@ -573,7 +489,7 @@ async function checkReviewEligibility(req, res, dependencies, config) {
|
|
|
573
489
|
|
|
574
490
|
try {
|
|
575
491
|
// Check for dev override impersonation
|
|
576
|
-
const { getEffectiveCid, getDevOverride } = require('
|
|
492
|
+
const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
|
|
577
493
|
const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
|
|
578
494
|
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
579
495
|
const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
|
|
@@ -589,7 +505,7 @@ async function checkReviewEligibility(req, res, dependencies, config) {
|
|
|
589
505
|
}
|
|
590
506
|
|
|
591
507
|
// Check if the effective user is a Popular Investor
|
|
592
|
-
const { checkIfUserIsPI } = require('
|
|
508
|
+
const { checkIfUserIsPI } = require('../data_helpers');
|
|
593
509
|
const rankEntry = await checkIfUserIsPI(db, effectiveCid, config, logger);
|
|
594
510
|
|
|
595
511
|
// Block Popular Investors from reviewing other Popular Investors
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
await requestRef.set(requestData);
|
|
48
|
+
|
|
49
|
+
logger.log('SUCCESS', `[requestPiAddition] User ${userCid} requested addition of PI ${piUsername} (CID: ${piCid || 'unknown'})`);
|
|
50
|
+
|
|
51
|
+
return res.status(201).json({
|
|
52
|
+
success: true,
|
|
53
|
+
requestId: requestId,
|
|
54
|
+
message: "Request submitted successfully"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.log('ERROR', `[requestPiAddition] Error submitting request`, error);
|
|
59
|
+
return res.status(500).json({ error: error.message });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* GET /user/me/watchlists/:id/rankings-check
|
|
65
|
+
* Check which PIs in a watchlist are in the latest available rankings
|
|
66
|
+
*/
|
|
67
|
+
async function checkPisInRankings(req, res, dependencies, config) {
|
|
68
|
+
const { db, logger } = dependencies;
|
|
69
|
+
const { userCid } = req.query;
|
|
70
|
+
const { id } = req.params;
|
|
71
|
+
|
|
72
|
+
if (!userCid || !id) {
|
|
73
|
+
return res.status(400).json({ error: "Missing userCid or watchlist id" });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Read watchlist from new path with migration
|
|
78
|
+
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
79
|
+
const watchlistResult = await readWithMigration(
|
|
80
|
+
db,
|
|
81
|
+
'signedInUsers',
|
|
82
|
+
'watchlists',
|
|
83
|
+
{ cid: userCid },
|
|
84
|
+
{
|
|
85
|
+
isCollection: false,
|
|
86
|
+
dataType: 'watchlists',
|
|
87
|
+
config,
|
|
88
|
+
logger,
|
|
89
|
+
documentId: id
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
let items = [];
|
|
94
|
+
if (watchlistResult && watchlistResult.data) {
|
|
95
|
+
items = watchlistResult.data.items || [];
|
|
96
|
+
} else {
|
|
97
|
+
// Fallback to legacy path
|
|
98
|
+
const watchlistsCollection = config.watchlistsCollection || 'watchlists';
|
|
99
|
+
const watchlistRef = db.collection(watchlistsCollection)
|
|
100
|
+
.doc(String(userCid))
|
|
101
|
+
.collection('lists')
|
|
102
|
+
.doc(id);
|
|
103
|
+
|
|
104
|
+
const watchlistDoc = await watchlistRef.get();
|
|
105
|
+
|
|
106
|
+
if (!watchlistDoc.exists) {
|
|
107
|
+
return res.status(404).json({ error: "Watchlist not found" });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const watchlistData = watchlistDoc.data();
|
|
111
|
+
items = watchlistData.items || [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find latest available rankings date
|
|
115
|
+
const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
116
|
+
const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
|
|
117
|
+
|
|
118
|
+
if (!rankingsDate) {
|
|
119
|
+
const notInRankings = items.map(item => item.cid);
|
|
120
|
+
return res.status(200).json({
|
|
121
|
+
notInRankings,
|
|
122
|
+
rankingsDate: null,
|
|
123
|
+
message: "No rankings data available"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fetch rankings data
|
|
128
|
+
const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
|
|
129
|
+
const rankingsDoc = await rankingsRef.get();
|
|
130
|
+
|
|
131
|
+
if (!rankingsDoc.exists) {
|
|
132
|
+
const notInRankings = items.map(item => item.cid);
|
|
133
|
+
return res.status(200).json({
|
|
134
|
+
notInRankings,
|
|
135
|
+
rankingsDate: null,
|
|
136
|
+
message: "Rankings document not found"
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const rankingsData = rankingsDoc.data();
|
|
141
|
+
const rankingsItems = rankingsData.Items || [];
|
|
142
|
+
|
|
143
|
+
// Create a set of CIDs that exist in rankings
|
|
144
|
+
const rankingsCIDs = new Set();
|
|
145
|
+
for (const item of rankingsItems) {
|
|
146
|
+
if (item.CustomerId) {
|
|
147
|
+
rankingsCIDs.add(String(item.CustomerId));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check which watchlist PIs are NOT in rankings
|
|
152
|
+
const notInRankings = [];
|
|
153
|
+
for (const item of items) {
|
|
154
|
+
const cidStr = String(item.cid);
|
|
155
|
+
if (!rankingsCIDs.has(cidStr)) {
|
|
156
|
+
notInRankings.push(item.cid);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return res.status(200).json({
|
|
161
|
+
notInRankings,
|
|
162
|
+
rankingsDate: rankingsDate,
|
|
163
|
+
totalChecked: items.length,
|
|
164
|
+
inRankings: items.length - notInRankings.length
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.log('ERROR', `[checkPisInRankings] Error checking PIs in rankings`, error);
|
|
169
|
+
return res.status(500).json({ error: error.message });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
requestPiAddition,
|
|
175
|
+
checkPisInRankings
|
|
176
|
+
};
|
|
177
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investor Search Helpers
|
|
3
|
+
* Handles PI search endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { findLatestRankingsDate } = require('../core/data_lookup_helpers');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /user/search/pis
|
|
10
|
+
* Search for Popular Investors by username
|
|
11
|
+
*/
|
|
12
|
+
async function searchPopularInvestors(req, res, dependencies, config) {
|
|
13
|
+
const { db, logger } = dependencies;
|
|
14
|
+
const { query, limit = 20 } = req.query;
|
|
15
|
+
|
|
16
|
+
if (!query || query.trim().length < 2) {
|
|
17
|
+
return res.status(400).json({ error: "Query must be at least 2 characters" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
22
|
+
const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
|
|
23
|
+
|
|
24
|
+
if (!rankingsDate) {
|
|
25
|
+
return res.status(404).json({ error: "Rankings data not available" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
|
|
29
|
+
const rankingsDoc = await rankingsRef.get();
|
|
30
|
+
|
|
31
|
+
if (!rankingsDoc.exists) {
|
|
32
|
+
return res.status(404).json({ error: "Rankings data not available" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rankingsData = rankingsDoc.data();
|
|
36
|
+
const rankingsItems = rankingsData.Items || [];
|
|
37
|
+
|
|
38
|
+
// Search by username (case-insensitive, partial match)
|
|
39
|
+
const searchQuery = query.toLowerCase().trim();
|
|
40
|
+
const matches = rankingsItems
|
|
41
|
+
.filter(item => {
|
|
42
|
+
const username = (item.UserName || '').toLowerCase();
|
|
43
|
+
return username.includes(searchQuery);
|
|
44
|
+
})
|
|
45
|
+
.slice(0, parseInt(limit))
|
|
46
|
+
.map(item => ({
|
|
47
|
+
cid: item.CustomerId,
|
|
48
|
+
username: item.UserName,
|
|
49
|
+
aum: item.AUMValue,
|
|
50
|
+
riskScore: item.RiskScore,
|
|
51
|
+
gain: item.Gain,
|
|
52
|
+
copiers: item.Copiers
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return res.status(200).json({
|
|
56
|
+
results: matches,
|
|
57
|
+
count: matches.length,
|
|
58
|
+
query: query
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.log('ERROR', `[searchPopularInvestors] Error searching PIs`, error);
|
|
63
|
+
return res.status(500).json({ error: error.message });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
searchPopularInvestors
|
|
69
|
+
};
|
|
70
|
+
|