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.
Files changed (49) hide show
  1. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
  2. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
  3. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
  4. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
  5. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
  6. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
  7. package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
  8. package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
  9. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
  10. package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
  11. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
  12. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
  13. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  14. package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
  15. package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
  16. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  17. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
  18. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
  19. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
  20. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
  21. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  22. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
  23. package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
  24. package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
  25. package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
  26. package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
  27. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
  28. package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
  29. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
  30. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
  31. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
  32. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
  33. package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
  34. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
  35. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
  36. package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
  37. package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
  38. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
  39. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
  40. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
  41. package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
  42. package/functions/generic-api/user-api/index.js +9 -9
  43. package/functions/task-engine/handler_creator.js +7 -6
  44. package/package.json +1 -1
  45. package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
  46. package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
  47. package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
  48. package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
  49. /package/functions/generic-api/user-api/helpers/{alert_helpers.js → alerts/alert_helpers.js} +0 -0
@@ -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('./dev_helpers');
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' && mergedData[key] !== null && value !== null) {
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('./data_helpers');
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('./dev_helpers');
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('./data_helpers');
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 { getCollectionPath, writeDual, readWithFallback } = require('./collection_helpers');
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
- // Write to both new and legacy structures
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
- let snapshot = null;
411
-
412
- // Try new structure first
413
- if (collectionRegistry) {
414
- try {
415
- const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
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
- let reviewData = null;
518
-
519
- // Try new structure first
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 = reviewData;
457
+
458
+ const data = reviewDoc.data();
543
459
  const review = {
544
- id: reviewId, // Use reviewId which is already defined
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('./dev_helpers');
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('./data_helpers');
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
+