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.
- package/functions/alert-system/helpers/alert_helpers.js +6 -6
- package/functions/alert-system/index.js +1 -1
- package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
- package/functions/api-v2/helpers/task_engine_helper.js +51 -0
- package/functions/api-v2/index.js +36 -0
- package/functions/api-v2/middleware/identity_middleware.js +48 -0
- package/functions/api-v2/package.json +6 -0
- package/functions/api-v2/routes/alerts.js +168 -0
- package/functions/api-v2/routes/index.js +35 -0
- package/functions/api-v2/routes/notifications.js +38 -0
- package/functions/api-v2/routes/popular_investors.js +204 -0
- package/functions/api-v2/routes/profile.js +212 -0
- package/functions/api-v2/routes/reviews.js +72 -0
- package/functions/api-v2/routes/settings.js +71 -0
- package/functions/api-v2/routes/sync.js +132 -0
- package/functions/api-v2/routes/verification.js +47 -0
- package/functions/api-v2/routes/watchlists.js +148 -0
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
- package/index.js +6 -2
- package/package.json +2 -1
- package/functions/generic-api/admin-api/index.js +0 -895
- package/functions/generic-api/helpers/api_helpers.js +0 -457
- package/functions/generic-api/index.js +0 -204
- package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
- package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
- package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
- package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
- package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
- package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
- package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
- package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
- package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
- package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
- package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
- package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
- 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,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;
|