bulltrackers-module 1.0.591 → 1.0.592

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 (68) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +6 -6
  2. package/functions/alert-system/index.js +1 -1
  3. package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
  4. package/functions/api-v2/helpers/task_engine_helper.js +51 -0
  5. package/functions/api-v2/index.js +36 -0
  6. package/functions/api-v2/middleware/identity_middleware.js +48 -0
  7. package/functions/api-v2/package.json +6 -0
  8. package/functions/api-v2/routes/alerts.js +168 -0
  9. package/functions/api-v2/routes/index.js +35 -0
  10. package/functions/api-v2/routes/notifications.js +38 -0
  11. package/functions/api-v2/routes/popular_investors.js +204 -0
  12. package/functions/api-v2/routes/profile.js +212 -0
  13. package/functions/api-v2/routes/reviews.js +72 -0
  14. package/functions/api-v2/routes/settings.js +71 -0
  15. package/functions/api-v2/routes/sync.js +132 -0
  16. package/functions/api-v2/routes/verification.js +47 -0
  17. package/functions/api-v2/routes/watchlists.js +148 -0
  18. package/functions/computation-system/helpers/computation_worker.js +1 -1
  19. package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
  20. package/index.js +6 -2
  21. package/package.json +2 -1
  22. package/functions/generic-api/admin-api/index.js +0 -895
  23. package/functions/generic-api/helpers/api_helpers.js +0 -457
  24. package/functions/generic-api/index.js +0 -204
  25. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
  26. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
  27. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
  28. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
  29. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
  30. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
  31. package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
  32. package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
  33. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
  34. package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
  35. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
  36. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
  37. package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
  38. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
  39. package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
  40. package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
  41. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
  42. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
  43. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
  44. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
  45. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
  46. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
  47. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
  48. package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
  49. package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
  50. package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
  51. package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
  52. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
  53. package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
  54. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
  55. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
  56. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
  57. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
  58. package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
  59. package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
  60. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
  61. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
  62. package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
  63. package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
  64. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
  65. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
  66. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
  67. package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
  68. package/functions/generic-api/user-api/index.js +0 -109
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Dispatch sync request to task engine
3
+ * @param {PubSub} pubsub - PubSub instance
4
+ * @param {number} cid - User/PI CID
5
+ * @param {string} username - Username
6
+ * @param {object} options - Options including type, requestId, requestedBy, etc.
7
+ */
8
+ const dispatchSyncRequest = async (pubsub, cid, username, options = {}) => {
9
+ const topicName = options.topicName || 'etoro-user-fetch-topic-ondemand';
10
+
11
+ // Calculate 7 days ago for since field
12
+ const sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString();
13
+
14
+ const payload = {
15
+ type: options.type, // 'ON_DEMAND_USER_UPDATE' | 'POPULAR_INVESTOR_UPDATE'
16
+ cid: cid,
17
+ username: username,
18
+ priority: options.priority || 'high',
19
+ source: options.source || 'on_demand_sync',
20
+ requestId: options.requestId,
21
+ requestedBy: options.requestedBy,
22
+ data: options.data || {
23
+ includeSocial: true,
24
+ since: sevenDaysAgo
25
+ },
26
+ metadata: {
27
+ onDemand: true,
28
+ targetCid: cid,
29
+ requestedAt: new Date().toISOString(),
30
+ userType: options.userType,
31
+ ...options.metadata
32
+ }
33
+ };
34
+
35
+ // Add optional fields if provided
36
+ if (options.effectiveRequestedBy !== undefined) {
37
+ payload.effectiveRequestedBy = options.effectiveRequestedBy;
38
+ }
39
+ if (options.actualRequestedBy !== undefined) {
40
+ payload.actualRequestedBy = options.actualRequestedBy;
41
+ }
42
+ if (options.userType && options.type === 'POPULAR_INVESTOR_UPDATE') {
43
+ payload.userType = options.userType; // Top-level field for PI updates
44
+ }
45
+
46
+ const dataBuffer = Buffer.from(JSON.stringify(payload));
47
+ const messageId = await pubsub.topic(topicName).publishMessage({ data: dataBuffer });
48
+ return messageId;
49
+ };
50
+
51
+ module.exports = { dispatchSyncRequest };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @fileoverview Main entry point for API v2
3
+ * Creates Express app and mounts all routes
4
+ */
5
+
6
+ const express = require('express');
7
+ const cors = require('cors');
8
+ const createRouter = require('./routes/index.js');
9
+
10
+ /**
11
+ * Main pipe: pipe.api.createApiV2App
12
+ * Creates Express app with API v2 routes
13
+ */
14
+ function createApiV2App(config, dependencies) {
15
+ const app = express();
16
+ const { logger } = dependencies;
17
+
18
+ // Middleware
19
+ app.use(cors({ origin: true }));
20
+ app.use(express.json());
21
+
22
+ // Health Check
23
+ app.get('/health', (req, res) => {
24
+ res.status(200).json({ status: 'OK', version: 'v2' });
25
+ });
26
+
27
+ // Mount API v2 routes
28
+ const router = createRouter(dependencies);
29
+ app.use('/', router);
30
+
31
+ logger.log('INFO', '[API v2] Routes mounted successfully');
32
+
33
+ return app;
34
+ }
35
+
36
+ module.exports = { createApiV2App };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Middleware to resolve the effective user ID, handling Developer Impersonation.
3
+ * Sets req.targetUserId, req.isImpersonating, and req.actualUserId.
4
+ */
5
+ const { isDeveloper } = require('../helpers/data-fetchers/firestore.js'); // Using your provided helper
6
+
7
+ const resolveUserIdentity = async (req, res, next) => {
8
+ try {
9
+ // 1. Identify the actual authenticated user (from Auth middleware or params)
10
+ const actualUserId = req.query.userCid || req.body.userCid || req.headers['x-user-cid'];
11
+
12
+ if (!actualUserId) {
13
+ return res.status(400).json({ error: "Missing user identification (userCid)" });
14
+ }
15
+
16
+ // 2. Check for Impersonation Request (Headers or Query)
17
+ const impersonateId = req.headers['x-impersonate-cid'] || req.query.impersonateCid;
18
+
19
+ req.actualUserId = actualUserId;
20
+ req.targetUserId = actualUserId; // Default to actual
21
+ req.isImpersonating = false;
22
+
23
+ // 3. specific logic for Developers
24
+ if (impersonateId && impersonateId !== actualUserId) {
25
+ // Verify if the ACTUAL user is a developer
26
+ // We pass the db instance from dependencies (assuming standard express injection)
27
+ const db = req.app.locals.db || req.dependencies.db;
28
+ const isDev = await isDeveloper(db, actualUserId);
29
+
30
+ if (isDev) {
31
+ req.targetUserId = impersonateId;
32
+ req.isImpersonating = true;
33
+ console.log(`[Identity] Dev ${actualUserId} is impersonating ${impersonateId}`);
34
+ } else {
35
+ // Fail silently or warn? For security, maybe just ignore or strict fail.
36
+ // Ignoring ensures they just see their own data.
37
+ console.warn(`[Identity] Unauthorized impersonation attempt by ${actualUserId}`);
38
+ }
39
+ }
40
+
41
+ next();
42
+ } catch (error) {
43
+ console.error('[IdentityMiddleware] Error:', error);
44
+ res.status(500).json({ error: "Internal Identity Error" });
45
+ }
46
+ };
47
+
48
+ module.exports = { resolveUserIdentity };
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "module",
3
+ "name": "api-v2",
4
+ "version": "1.0.0",
5
+ "description": "API v2 - Refactored user API with middleware architecture"
6
+ }
@@ -0,0 +1,168 @@
1
+ const express = require('express');
2
+ const {
3
+ fetchUserAlerts,
4
+ subscribeToWatchlistAlerts,
5
+ fetchAlertTypes,
6
+ getUserSubscriptions,
7
+ updateSubscription,
8
+ unsubscribeFromAlerts
9
+ } = require('../helpers/data-fetchers/firestore.js');
10
+
11
+ const router = express.Router();
12
+
13
+ // GET /alerts/history
14
+ router.get('/history', async (req, res) => {
15
+ try {
16
+ const { db } = req.dependencies;
17
+ const { limit, unreadOnly, type } = req.query;
18
+ const alerts = await fetchUserAlerts(db, req.targetUserId, {
19
+ limit: parseInt(limit || 50),
20
+ unreadOnly: unreadOnly === 'true',
21
+ type
22
+ });
23
+ res.json({ success: true, count: alerts.length, data: alerts });
24
+ } catch (error) {
25
+ res.status(500).json({ error: error.message });
26
+ }
27
+ });
28
+
29
+ // GET /alerts/count (Rec 13)
30
+ router.get('/count', async (req, res) => {
31
+ try {
32
+ const { db } = req.dependencies;
33
+ const alerts = await fetchUserAlerts(db, req.targetUserId, { unreadOnly: true, limit: 1000 });
34
+ res.json({ success: true, count: alerts.length });
35
+ } catch (error) {
36
+ res.status(500).json({ error: error.message });
37
+ }
38
+ });
39
+
40
+ // PUT /alerts/:id/read (Rec 13)
41
+ router.put('/:id/read', async (req, res) => {
42
+ try {
43
+ const { db } = req.dependencies;
44
+ await db.collection('user_alerts').doc(req.targetUserId).collection('alerts').doc(req.params.id)
45
+ .update({ read: true, readAt: new Date() });
46
+ res.json({ success: true });
47
+ } catch (e) { res.status(500).json({ error: e.message }); }
48
+ });
49
+
50
+ // PUT /alerts/read-all (Rec 13)
51
+ router.put('/read-all', async (req, res) => {
52
+ try {
53
+ const { db } = req.dependencies;
54
+ const batch = db.batch();
55
+ const snaps = await db.collection('user_alerts').doc(req.targetUserId).collection('alerts').where('read', '==', false).get();
56
+ snaps.docs.forEach(doc => batch.update(doc.ref, { read: true, readAt: new Date() }));
57
+ await batch.commit();
58
+ res.json({ success: true, updated: snaps.size });
59
+ } catch (e) { res.status(500).json({ error: e.message }); }
60
+ });
61
+
62
+ // DELETE /alerts/:id (Rec 13)
63
+ router.delete('/:id', async (req, res) => {
64
+ try {
65
+ const { db } = req.dependencies;
66
+ await db.collection('user_alerts').doc(req.targetUserId).collection('alerts').doc(req.params.id).delete();
67
+ res.json({ success: true });
68
+ } catch (e) { res.status(500).json({ error: e.message }); }
69
+ });
70
+
71
+ // POST /alerts/subscribe
72
+ router.post('/subscribe', async (req, res) => {
73
+ try {
74
+ const { db } = req.dependencies;
75
+ const { watchlistId, piId, alertConfig } = req.body;
76
+ const result = await subscribeToWatchlistAlerts(db, req.targetUserId, watchlistId, piId, alertConfig);
77
+ res.json(result);
78
+ } catch (error) {
79
+ res.status(500).json({ error: error.message });
80
+ }
81
+ });
82
+
83
+ // GET /alerts/types
84
+ router.get('/types', async (req, res) => {
85
+ const data = await fetchAlertTypes();
86
+ res.json({ success: true, data });
87
+ });
88
+
89
+ // GET /alerts/dynamic-watchlist-computations - Get available computations for dynamic watchlists
90
+ router.get('/dynamic-watchlist-computations', async (req, res) => {
91
+ try {
92
+ // Import getAllAlertTypes from alert system
93
+ const { getAllAlertTypes } = require('../../../../alert-system/helpers/alert_type_registry.js');
94
+ const alertTypes = getAllAlertTypes();
95
+
96
+ // Extract unique computations from alert types
97
+ const computations = alertTypes.map(type => ({
98
+ computationName: type.computationName,
99
+ alertTypeName: type.name,
100
+ description: type.description,
101
+ severity: type.severity
102
+ }));
103
+
104
+ res.json({
105
+ success: true,
106
+ computations
107
+ });
108
+ } catch (error) {
109
+ res.status(500).json({ error: error.message });
110
+ }
111
+ });
112
+
113
+ // GET /alerts/subscriptions - Get all user subscriptions
114
+ router.get('/subscriptions', async (req, res) => {
115
+ try {
116
+ const { db } = req.dependencies;
117
+ const subscriptions = await getUserSubscriptions(db, req.targetUserId);
118
+ res.json({
119
+ success: true,
120
+ subscriptions,
121
+ count: subscriptions.length
122
+ });
123
+ } catch (error) {
124
+ res.status(500).json({ error: error.message });
125
+ }
126
+ });
127
+
128
+ // PUT /alerts/subscriptions/:piCid - Update subscription settings
129
+ router.put('/subscriptions/:piCid', async (req, res) => {
130
+ try {
131
+ const { db } = req.dependencies;
132
+ const { piCid } = req.params;
133
+ const { alertTypes, thresholds } = req.body;
134
+
135
+ const updates = {};
136
+ if (alertTypes !== undefined) updates.alertTypes = alertTypes;
137
+ if (thresholds !== undefined) updates.thresholds = thresholds;
138
+
139
+ if (Object.keys(updates).length === 0) {
140
+ return res.status(400).json({ error: "No updates provided" });
141
+ }
142
+
143
+ const subscription = await updateSubscription(db, req.targetUserId, piCid, updates);
144
+ res.json({ success: true, subscription });
145
+ } catch (error) {
146
+ if (error.message === "Subscription not found") {
147
+ return res.status(404).json({ error: error.message });
148
+ }
149
+ res.status(500).json({ error: error.message });
150
+ }
151
+ });
152
+
153
+ // DELETE /alerts/subscriptions/:piCid - Unsubscribe from a PI
154
+ router.delete('/subscriptions/:piCid', async (req, res) => {
155
+ try {
156
+ const { db } = req.dependencies;
157
+ const { piCid } = req.params;
158
+ await unsubscribeFromAlerts(db, req.targetUserId, piCid);
159
+ res.json({ success: true, message: "Unsubscribed successfully" });
160
+ } catch (error) {
161
+ if (error.message === "Subscription not found") {
162
+ return res.status(404).json({ error: error.message });
163
+ }
164
+ res.status(500).json({ error: error.message });
165
+ }
166
+ });
167
+
168
+ module.exports = router;
@@ -0,0 +1,35 @@
1
+ const express = require('express');
2
+ const { resolveUserIdentity } = require('../middleware/identity_middleware.js');
3
+
4
+ const notificationRoutes = require('./notifications.js');
5
+ const alertsRoutes = require('./alerts.js'); // <--- NEW
6
+ const verificationRoutes = require('./verification.js');
7
+ const profileRoutes = require('./profile.js');
8
+ const piRoutes = require('./popular_investors.js');
9
+ const watchlistRoutes = require('./watchlists.js');
10
+ const syncRoutes = require('./sync.js');
11
+ const settingsRoutes = require('./settings.js');
12
+ const reviewsRoutes = require('./reviews.js');
13
+
14
+ module.exports = (dependencies) => {
15
+ const router = express.Router();
16
+
17
+ router.use((req, res, next) => {
18
+ req.dependencies = dependencies;
19
+ next();
20
+ });
21
+
22
+ router.use(resolveUserIdentity);
23
+
24
+ router.use('/notifications', notificationRoutes);
25
+ router.use('/alerts', alertsRoutes); // <--- NEW
26
+ router.use('/verification', verificationRoutes);
27
+ router.use('/profile', profileRoutes);
28
+ router.use('/popular-investors', piRoutes);
29
+ router.use('/watchlists', watchlistRoutes);
30
+ router.use('/sync', syncRoutes);
31
+ router.use('/settings', settingsRoutes);
32
+ router.use('/reviews', reviewsRoutes);
33
+
34
+ return router;
35
+ };
@@ -0,0 +1,38 @@
1
+ const express = require('express');
2
+ // Note: You need to add fetchNotifications & markRead to your firestore.js helpers
3
+ const { fetchNotifications, markNotificationRead } = require('../helpers/data-fetchers/firestore.js');
4
+
5
+ const router = express.Router();
6
+
7
+ // GET /notifications/history
8
+ router.get('/history', async (req, res) => {
9
+ try {
10
+ const { db } = req.dependencies;
11
+ const { limit = 20, unreadOnly = false } = req.query;
12
+
13
+ const notifications = await fetchNotifications(db, req.targetUserId, {
14
+ limit: parseInt(limit),
15
+ unreadOnly: unreadOnly === 'true'
16
+ });
17
+
18
+ res.json({ success: true, count: notifications.length, data: notifications });
19
+ } catch (error) {
20
+ res.status(500).json({ error: error.message });
21
+ }
22
+ });
23
+
24
+ // POST /notifications/mark-read
25
+ router.post('/mark-read', async (req, res) => {
26
+ try {
27
+ const { db } = req.dependencies;
28
+ const { notificationIds, markAll } = req.body; // Array of IDs or boolean flag
29
+
30
+ await markNotificationRead(db, req.targetUserId, notificationIds, markAll);
31
+
32
+ res.json({ success: true });
33
+ } catch (error) {
34
+ res.status(500).json({ error: error.message });
35
+ }
36
+ });
37
+
38
+ module.exports = router;
@@ -0,0 +1,204 @@
1
+ const express = require('express');
2
+ const {
3
+ fetchPopularInvestorMasterList,
4
+ fetchTrendingPopularInvestors,
5
+ fetchPopularInvestorCategories,
6
+ searchPopularInvestors,
7
+ getComputationResults,
8
+ requestPopularInvestorAddition,
9
+ trackPopularInvestorView,
10
+ pageCollection
11
+ } = require('../helpers/data-fetchers/firestore.js');
12
+
13
+ const router = express.Router();
14
+
15
+
16
+ // GET /popular-investors/master-list
17
+ router.get('/master-list', async (req, res) => {
18
+ try {
19
+ const { db } = req.dependencies;
20
+ const data = await fetchPopularInvestorMasterList(db);
21
+ res.json({ success: true, count: Object.keys(data).length, data });
22
+ } catch (error) {
23
+ res.status(500).json({ error: error.message });
24
+ }
25
+ });
26
+
27
+ // GET /popular-investors/rankings
28
+ router.get('/rankings', async (req, res) => {
29
+ try {
30
+ const { db } = req.dependencies;
31
+ const { date } = req.query;
32
+ // Using generic computation fetcher for rankings
33
+ const rankings = await getComputationResults(db, 'PopularInvestorRankings', date);
34
+ res.json({ success: true, data: rankings });
35
+ } catch (error) {
36
+ res.status(500).json({ error: error.message });
37
+ }
38
+ });
39
+
40
+ // POST /popular-investors/request-addition
41
+ router.post('/request-addition', async (req, res) => {
42
+ try {
43
+ const { db } = req.dependencies;
44
+ const { piId, piUsername } = req.body;
45
+
46
+ const result = await requestPopularInvestorAddition(db, req.targetUserId, piId, piUsername);
47
+ res.json(result);
48
+ } catch (error) {
49
+ res.status(400).json({ error: error.message });
50
+ }
51
+ });
52
+
53
+ // POST /popular-investors/:piId/track-view
54
+ router.post('/:piId/track-view', async (req, res) => {
55
+ try {
56
+ const { db } = req.dependencies;
57
+ const { piId } = req.params;
58
+ const { viewerType } = req.body; // e.g., 'anonymous', 'signed_in'
59
+
60
+ // req.targetUserId comes from the identity middleware.
61
+ // If the user is anonymous (no token), middleware might leave it null or we check viewerType.
62
+ // Assuming identity middleware populates it if available.
63
+ const viewerId = req.targetUserId || null;
64
+
65
+ await trackPopularInvestorView(db, piId, viewerId, viewerType);
66
+
67
+ res.json({ success: true });
68
+ } catch (error) {
69
+ // We generally don't want to break the client if stats fail, but we log it
70
+ console.error(`Tracking error: ${error.message}`);
71
+ res.status(200).json({ success: false, message: "Tracking failed silently" });
72
+ }
73
+ });
74
+
75
+ // GET /popular-investors/trending
76
+ router.get('/trending', async (req, res) => {
77
+ try {
78
+ const { db } = req.dependencies;
79
+ const data = await fetchTrendingPopularInvestors(db);
80
+ res.json({ success: true, count: data.length, data });
81
+ } catch (error) {
82
+ res.status(500).json({ error: error.message });
83
+ }
84
+ });
85
+
86
+ // GET /popular-investors/categories
87
+ router.get('/categories', async (req, res) => {
88
+ try {
89
+ const { db } = req.dependencies;
90
+ const data = await fetchPopularInvestorCategories(db);
91
+ res.json({ success: true, data });
92
+ } catch (error) {
93
+ res.status(500).json({ error: error.message });
94
+ }
95
+ });
96
+
97
+ // GET /popular-investors/search
98
+ router.get('/search', async (req, res) => {
99
+ try {
100
+ const { db } = req.dependencies;
101
+ const { query } = req.query;
102
+ if (!query || query.length < 2) return res.status(400).json({ error: "Query too short" });
103
+
104
+ const results = await searchPopularInvestors(db, query);
105
+ res.json({ success: true, count: results.length, data: results });
106
+ } catch (error) {
107
+ res.status(500).json({ error: error.message });
108
+ }
109
+ });
110
+
111
+ /**
112
+ * PUBLIC PROFILE PAGE - Popular Investor Profile (Visible to All Signed-in Users)
113
+ *
114
+ * These routes return public profile data for any Popular Investor.
115
+ * - Uses PopularInvestorProfileMetrics computation
116
+ * - Access: Public (any signed-in user can view any PI's profile)
117
+ *
118
+ * Note: "Public" means visible to all signed-in users, not anonymous users.
119
+ * All routes require authentication via identity middleware.
120
+ */
121
+
122
+ // GET /popular-investors/:piId/profile - Public PI profile page
123
+ router.get('/:piId/profile', async (req, res) => {
124
+ try {
125
+ const { db } = req.dependencies;
126
+ const { piId } = req.params;
127
+ const { date, lookback = 7 } = req.query;
128
+
129
+ if (!piId) {
130
+ return res.status(400).json({ error: "PI ID is required" });
131
+ }
132
+
133
+ // Default to today if no date provided
134
+ const targetDate = date || new Date().toISOString().split('T')[0];
135
+
136
+ // Verify the PI exists in the master list
137
+ try {
138
+ await fetchPopularInvestorMasterList(db, piId);
139
+ } catch (e) {
140
+ return res.status(404).json({
141
+ error: "Popular Investor not found",
142
+ message: `PI with ID ${piId} is not in the master list`
143
+ });
144
+ }
145
+
146
+ // Fetch profile data from PopularInvestorProfileMetrics computation
147
+ const computationName = 'PopularInvestorProfileMetrics';
148
+ const profileData = await pageCollection(db, targetDate, computationName, piId, parseInt(lookback));
149
+
150
+ res.json({
151
+ success: true,
152
+ computation: computationName,
153
+ piId: piId,
154
+ data: profileData,
155
+ profileType: 'public' // Indicates this is a public PI profile
156
+ });
157
+ } catch (error) {
158
+ res.status(404).json({ error: error.message });
159
+ }
160
+ });
161
+
162
+ // GET /popular-investors/:piId/analytics - PI analytics summary
163
+ router.get('/:piId/analytics', async (req, res) => {
164
+ try {
165
+ const { db } = req.dependencies;
166
+ const { piId } = req.params;
167
+
168
+ if (!piId) {
169
+ return res.status(400).json({ error: "PI ID is required" });
170
+ }
171
+
172
+ // Verify the PI exists in the master list
173
+ try {
174
+ await fetchPopularInvestorMasterList(db, piId);
175
+ } catch (e) {
176
+ return res.status(404).json({
177
+ error: "Popular Investor not found",
178
+ message: `PI with ID ${piId} is not in the master list`
179
+ });
180
+ }
181
+
182
+ // Try to fetch from pi_analytics_summary collection (legacy)
183
+ // This is pre-computed analytics data
184
+ const analyticsDoc = await db.collection('pi_analytics_summary').doc(String(piId)).get();
185
+
186
+ if (!analyticsDoc.exists) {
187
+ return res.status(404).json({
188
+ error: "Analytics not found",
189
+ message: `No analytics data available for PI ${piId}`
190
+ });
191
+ }
192
+
193
+ res.json({
194
+ success: true,
195
+ piId: piId,
196
+ data: analyticsDoc.data()
197
+ });
198
+ } catch (error) {
199
+ res.status(500).json({ error: error.message });
200
+ }
201
+ });
202
+
203
+
204
+ module.exports = router;