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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Popular Investor Profile Helpers
|
|
3
|
+
* Handles PI profile and analytics endpoints with migration support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { findLatestComputationDate } = require('../core/data_lookup_helpers');
|
|
7
|
+
const { checkPiInComputationDate } = require('../data/computation_helpers');
|
|
8
|
+
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /pi/:cid/analytics
|
|
12
|
+
* Fetches pre-computed analytics from the 'analytics_results' collection
|
|
13
|
+
*/
|
|
14
|
+
async function getPiAnalytics(req, res, dependencies, config) {
|
|
15
|
+
const { db } = dependencies;
|
|
16
|
+
const { cid } = req.params;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Try new path first with migration
|
|
20
|
+
const result = await readWithMigration(
|
|
21
|
+
db,
|
|
22
|
+
'popularInvestors',
|
|
23
|
+
'analytics',
|
|
24
|
+
{ piCid: cid },
|
|
25
|
+
{
|
|
26
|
+
isCollection: false,
|
|
27
|
+
dataType: 'piAnalytics',
|
|
28
|
+
config,
|
|
29
|
+
documentId: String(cid)
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (result && result.data) {
|
|
34
|
+
return res.status(200).json(result.data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fallback to legacy collection
|
|
38
|
+
const docRef = db.collection(config.piAnalyticsSummaryCollection || 'pi_analytics_summary').doc(String(cid));
|
|
39
|
+
const doc = await docRef.get();
|
|
40
|
+
|
|
41
|
+
if (!doc.exists) {
|
|
42
|
+
return res.status(404).json({ error: "No analytics found for this user." });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return res.status(200).json(doc.data());
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return res.status(500).json({ error: error.message });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* GET /pi/:cid/profile
|
|
54
|
+
* Fetches Popular Investor profile data from computation
|
|
55
|
+
* Falls back to latest available date if today's data doesn't exist
|
|
56
|
+
*/
|
|
57
|
+
async function getPiProfile(req, res, dependencies, config) {
|
|
58
|
+
const { db, logger } = dependencies;
|
|
59
|
+
const { cid } = req.params;
|
|
60
|
+
|
|
61
|
+
if (!cid) {
|
|
62
|
+
return res.status(400).json({ error: "Missing PI CID" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
|
|
67
|
+
const resultsSub = config.resultsSubcollection || 'results';
|
|
68
|
+
const compsSub = config.computationsSubcollection || 'computations';
|
|
69
|
+
const computationName = 'PopularInvestorProfileMetrics';
|
|
70
|
+
const category = 'popular-investor';
|
|
71
|
+
const today = new Date().toISOString().split('T')[0];
|
|
72
|
+
const cidStr = String(cid);
|
|
73
|
+
const maxDaysBackForPi = 7;
|
|
74
|
+
|
|
75
|
+
logger.log('INFO', `[getPiProfile] Starting search for PI CID: ${cid}`);
|
|
76
|
+
|
|
77
|
+
// Find latest available computation date
|
|
78
|
+
const latestDate = await findLatestComputationDate(
|
|
79
|
+
db,
|
|
80
|
+
insightsCollection,
|
|
81
|
+
resultsSub,
|
|
82
|
+
compsSub,
|
|
83
|
+
category,
|
|
84
|
+
computationName,
|
|
85
|
+
null,
|
|
86
|
+
30
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
logger.log('INFO', `[getPiProfile] Latest computation date found: ${latestDate || 'NONE'}`);
|
|
90
|
+
|
|
91
|
+
if (!latestDate) {
|
|
92
|
+
logger.log('WARN', `[getPiProfile] No computation document found for ${computationName} in last 30 days`);
|
|
93
|
+
return res.status(404).json({
|
|
94
|
+
error: "Profile data not available",
|
|
95
|
+
message: "No computation results found for this Popular Investor"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try to find the PI starting from the latest date, then going back up to 7 days
|
|
100
|
+
let foundDate = null;
|
|
101
|
+
let profileData = null;
|
|
102
|
+
let checkedDates = [];
|
|
103
|
+
|
|
104
|
+
const latestDateObj = new Date(latestDate + 'T00:00:00Z');
|
|
105
|
+
|
|
106
|
+
for (let daysBack = 0; daysBack <= maxDaysBackForPi; daysBack++) {
|
|
107
|
+
const checkDate = new Date(latestDateObj);
|
|
108
|
+
checkDate.setUTCDate(latestDateObj.getUTCDate() - daysBack);
|
|
109
|
+
const dateStr = checkDate.toISOString().split('T')[0];
|
|
110
|
+
checkedDates.push(dateStr);
|
|
111
|
+
|
|
112
|
+
logger.log('INFO', `[getPiProfile] Checking date ${dateStr} for CID ${cid} (${daysBack} days back from latest)`);
|
|
113
|
+
|
|
114
|
+
const result = await checkPiInComputationDate(
|
|
115
|
+
db,
|
|
116
|
+
insightsCollection,
|
|
117
|
+
resultsSub,
|
|
118
|
+
compsSub,
|
|
119
|
+
category,
|
|
120
|
+
computationName,
|
|
121
|
+
dateStr,
|
|
122
|
+
cidStr,
|
|
123
|
+
logger
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (result.found) {
|
|
127
|
+
foundDate = dateStr;
|
|
128
|
+
profileData = result.profileData;
|
|
129
|
+
logger.log('SUCCESS', `[getPiProfile] Found profile data for CID ${cid} in date ${dateStr} (${daysBack} days back from latest)`);
|
|
130
|
+
break;
|
|
131
|
+
} else {
|
|
132
|
+
logger.log('INFO', `[getPiProfile] CID ${cid} not found in date ${dateStr}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// If not found in any checked date, return 404
|
|
137
|
+
if (!foundDate || !profileData) {
|
|
138
|
+
logger.log('WARN', `[getPiProfile] CID ${cid} not found in any checked dates: ${checkedDates.join(', ')}`);
|
|
139
|
+
|
|
140
|
+
// Try to get sample data from the latest date to show what CIDs are available
|
|
141
|
+
const latestResult = await checkPiInComputationDate(
|
|
142
|
+
db,
|
|
143
|
+
insightsCollection,
|
|
144
|
+
resultsSub,
|
|
145
|
+
compsSub,
|
|
146
|
+
category,
|
|
147
|
+
computationName,
|
|
148
|
+
latestDate,
|
|
149
|
+
cidStr,
|
|
150
|
+
logger
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const allAvailableCids = latestResult.computationData && typeof latestResult.computationData === 'object' && !Array.isArray(latestResult.computationData)
|
|
154
|
+
? Object.keys(latestResult.computationData)
|
|
155
|
+
.filter(key => !key.startsWith('_'))
|
|
156
|
+
.sort()
|
|
157
|
+
: [];
|
|
158
|
+
|
|
159
|
+
return res.status(404).json({
|
|
160
|
+
error: "Profile data not found",
|
|
161
|
+
message: `Popular Investor ${cid} does not exist in computation results for the last ${maxDaysBackForPi + 1} days. This PI may not have been processed recently.`,
|
|
162
|
+
debug: {
|
|
163
|
+
searchedCid: cidStr,
|
|
164
|
+
checkedDates: checkedDates,
|
|
165
|
+
totalCidsInLatestDocument: allAvailableCids.length,
|
|
166
|
+
sampleAvailableCids: allAvailableCids.slice(0, 20),
|
|
167
|
+
latestDate: latestDate
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get username from rankings
|
|
173
|
+
const { getPiUsername } = require('../on_demand_fetch_helpers');
|
|
174
|
+
const username = await getPiUsername(db, cid, config, logger);
|
|
175
|
+
|
|
176
|
+
logger.log('SUCCESS', `[getPiProfile] Returning profile data for CID ${cid} from date ${foundDate} (requested: ${today}, latest available: ${latestDate})`);
|
|
177
|
+
|
|
178
|
+
return res.status(200).json({
|
|
179
|
+
status: 'success',
|
|
180
|
+
cid: cidStr,
|
|
181
|
+
username: username || null,
|
|
182
|
+
data: profileData,
|
|
183
|
+
isFallback: foundDate !== latestDate || foundDate !== today,
|
|
184
|
+
dataDate: foundDate,
|
|
185
|
+
latestComputationDate: latestDate,
|
|
186
|
+
requestedDate: today,
|
|
187
|
+
daysBackFromLatest: checkedDates.indexOf(foundDate)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.log('ERROR', `[getPiProfile] Error fetching PI profile for ${cid}`, error);
|
|
192
|
+
return res.status(500).json({ error: error.message });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
getPiAnalytics,
|
|
198
|
+
getPiProfile
|
|
199
|
+
};
|
|
200
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Profile View Tracking Helpers
|
|
3
|
+
* Handles profile view tracking with migration support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { FieldValue } = require('@google-cloud/firestore');
|
|
7
|
+
const { readWithMigration, writeWithMigration, getCidFromFirebaseUid } = require('../core/path_resolution_helpers');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /pi/:piCid/track-view
|
|
11
|
+
* Tracks a profile view for a Popular Investor
|
|
12
|
+
* Migrates from legacy path to new path automatically
|
|
13
|
+
*/
|
|
14
|
+
async function trackProfileView(req, res, dependencies, config) {
|
|
15
|
+
const { db, logger } = dependencies;
|
|
16
|
+
const { piCid } = req.params;
|
|
17
|
+
const { viewerCid, viewerType = 'anonymous', firebaseUid } = req.body;
|
|
18
|
+
|
|
19
|
+
if (!piCid) {
|
|
20
|
+
return res.status(400).json({ error: "Missing piCid" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// If firebaseUid is provided, get CID
|
|
25
|
+
let effectiveViewerCid = viewerCid;
|
|
26
|
+
if (firebaseUid && !viewerCid) {
|
|
27
|
+
effectiveViewerCid = await getCidFromFirebaseUid(db, firebaseUid);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const today = new Date().toISOString().split('T')[0];
|
|
31
|
+
const timestamp = Date.now();
|
|
32
|
+
|
|
33
|
+
// Create/update daily view document (new path)
|
|
34
|
+
const viewData = {
|
|
35
|
+
piCid: Number(piCid),
|
|
36
|
+
date: today,
|
|
37
|
+
totalViews: FieldValue.increment(1),
|
|
38
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Add unique viewers if viewer CID is provided
|
|
42
|
+
if (effectiveViewerCid) {
|
|
43
|
+
// Read existing to merge unique viewers
|
|
44
|
+
const existingResult = await readWithMigration(
|
|
45
|
+
db,
|
|
46
|
+
'popularInvestors',
|
|
47
|
+
'profileViews',
|
|
48
|
+
{ piCid, date: today },
|
|
49
|
+
{
|
|
50
|
+
isCollection: false,
|
|
51
|
+
dataType: 'piProfileViews',
|
|
52
|
+
config,
|
|
53
|
+
documentId: today
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const existingUniqueViewers = existingResult?.data?.uniqueViewers || [];
|
|
58
|
+
const viewerCidStr = String(effectiveViewerCid);
|
|
59
|
+
|
|
60
|
+
if (!existingUniqueViewers.includes(viewerCidStr)) {
|
|
61
|
+
viewData.uniqueViewers = [...existingUniqueViewers, viewerCidStr];
|
|
62
|
+
} else {
|
|
63
|
+
viewData.uniqueViewers = existingUniqueViewers;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Write to new path (with dual write to legacy during migration)
|
|
68
|
+
await writeWithMigration(
|
|
69
|
+
db,
|
|
70
|
+
'popularInvestors',
|
|
71
|
+
'profileViews',
|
|
72
|
+
{ piCid, date: today },
|
|
73
|
+
viewData,
|
|
74
|
+
{
|
|
75
|
+
isCollection: false,
|
|
76
|
+
merge: true,
|
|
77
|
+
dataType: 'piProfileViews',
|
|
78
|
+
config,
|
|
79
|
+
documentId: today,
|
|
80
|
+
dualWrite: true
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Track individual view if viewer CID is provided
|
|
85
|
+
if (effectiveViewerCid) {
|
|
86
|
+
const viewId = `${piCid}_${effectiveViewerCid}_${timestamp}`;
|
|
87
|
+
const individualViewData = {
|
|
88
|
+
piCid: Number(piCid),
|
|
89
|
+
viewerCid: Number(effectiveViewerCid),
|
|
90
|
+
viewerType: viewerType,
|
|
91
|
+
viewedAt: FieldValue.serverTimestamp(),
|
|
92
|
+
date: today
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Write individual view (new path)
|
|
96
|
+
await writeWithMigration(
|
|
97
|
+
db,
|
|
98
|
+
'popularInvestors',
|
|
99
|
+
'individualViews',
|
|
100
|
+
{ piCid, viewId },
|
|
101
|
+
individualViewData,
|
|
102
|
+
{
|
|
103
|
+
isCollection: false,
|
|
104
|
+
merge: true,
|
|
105
|
+
dataType: 'piIndividualViews',
|
|
106
|
+
config,
|
|
107
|
+
documentId: viewId,
|
|
108
|
+
dualWrite: true
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return res.status(200).json({ success: true, message: "View tracked" });
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.log('ERROR', `[trackProfileView] Error tracking view for PI ${piCid}:`, error);
|
|
117
|
+
// Don't fail the request if tracking fails
|
|
118
|
+
return res.status(200).json({ success: false, message: "View tracking failed but request succeeded" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
trackProfileView
|
|
124
|
+
};
|
|
125
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview User Profile Helpers
|
|
3
|
+
* Handles signed-in user profile endpoints with migration support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { checkIfUserIsPI } = require('../core/user_status_helpers');
|
|
7
|
+
const { readWithMigration, getCidFromFirebaseUid } = require('../core/path_resolution_helpers');
|
|
8
|
+
const { getEffectiveCid, getDevOverride } = require('../dev_helpers');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /user/me/verification
|
|
12
|
+
* Fetches the signed-in user's verification data (includes avatar URL)
|
|
13
|
+
* Uses migration to read from new path or legacy path
|
|
14
|
+
*/
|
|
15
|
+
async function getUserVerification(req, res, dependencies, config) {
|
|
16
|
+
const { db, logger } = dependencies;
|
|
17
|
+
const { userCid, firebaseUid } = req.query;
|
|
18
|
+
|
|
19
|
+
if (!userCid && !firebaseUid) {
|
|
20
|
+
return res.status(400).json({ error: "Missing userCid or firebaseUid" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Get CID if firebaseUid provided
|
|
25
|
+
let effectiveCid = userCid ? Number(userCid) : null;
|
|
26
|
+
if (firebaseUid && !effectiveCid) {
|
|
27
|
+
effectiveCid = await getCidFromFirebaseUid(db, firebaseUid);
|
|
28
|
+
if (!effectiveCid) {
|
|
29
|
+
return res.status(404).json({ error: "User not found" });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check for dev override impersonation
|
|
34
|
+
const devOverride = await getDevOverride(db, effectiveCid, config, logger);
|
|
35
|
+
const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
|
|
36
|
+
|
|
37
|
+
if (isImpersonating) {
|
|
38
|
+
effectiveCid = devOverride.impersonateCid;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If impersonating a PI, try to get username from rankings
|
|
42
|
+
if (isImpersonating) {
|
|
43
|
+
const rankEntry = await checkIfUserIsPI(db, effectiveCid, config, logger);
|
|
44
|
+
if (rankEntry) {
|
|
45
|
+
return res.status(200).json({
|
|
46
|
+
avatar: null,
|
|
47
|
+
username: rankEntry.UserName || null,
|
|
48
|
+
fullName: null,
|
|
49
|
+
cid: effectiveCid,
|
|
50
|
+
verifiedAt: null,
|
|
51
|
+
isImpersonating: true,
|
|
52
|
+
effectiveCid: effectiveCid,
|
|
53
|
+
actualCid: Number(userCid)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Try to read from new path with migration
|
|
59
|
+
const result = await readWithMigration(
|
|
60
|
+
db,
|
|
61
|
+
'signedInUsers',
|
|
62
|
+
'verification',
|
|
63
|
+
{ cid: effectiveCid },
|
|
64
|
+
{
|
|
65
|
+
isCollection: false,
|
|
66
|
+
dataType: 'verification',
|
|
67
|
+
config,
|
|
68
|
+
logger,
|
|
69
|
+
documentId: 'data'
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (result && result.data) {
|
|
74
|
+
return res.status(200).json({
|
|
75
|
+
avatar: result.data.avatar || null,
|
|
76
|
+
username: result.data.username || null,
|
|
77
|
+
fullName: result.data.fullName || null,
|
|
78
|
+
cid: result.data.cid || effectiveCid,
|
|
79
|
+
verifiedAt: result.data.verifiedAt || null,
|
|
80
|
+
isImpersonating: isImpersonating || false,
|
|
81
|
+
effectiveCid: effectiveCid,
|
|
82
|
+
actualCid: Number(userCid),
|
|
83
|
+
migrated: result.source === 'legacy'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback to legacy: try reading from signedInUsers main document
|
|
88
|
+
const { signedInUsersCollection } = config;
|
|
89
|
+
const userDocRef = db.collection(signedInUsersCollection).doc(String(effectiveCid));
|
|
90
|
+
const userDoc = await userDocRef.get();
|
|
91
|
+
|
|
92
|
+
if (!userDoc.exists) {
|
|
93
|
+
return res.status(404).json({ error: "User verification data not found" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const data = userDoc.data();
|
|
97
|
+
|
|
98
|
+
return res.status(200).json({
|
|
99
|
+
avatar: data.avatar || null,
|
|
100
|
+
username: data.username || data.etoroUsername || null,
|
|
101
|
+
fullName: data.displayName || null,
|
|
102
|
+
cid: data.cid || data.etoroCID || effectiveCid,
|
|
103
|
+
verifiedAt: data.verifiedAt || null,
|
|
104
|
+
isImpersonating: isImpersonating || false,
|
|
105
|
+
effectiveCid: effectiveCid,
|
|
106
|
+
actualCid: Number(userCid)
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.log('ERROR', `[getUserVerification] Error fetching verification for ${userCid}`, error);
|
|
111
|
+
return res.status(500).json({ error: error.message });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* GET /user/me/is-popular-investor
|
|
117
|
+
* Check if signed-in user is also a Popular Investor
|
|
118
|
+
* Supports dev override impersonation
|
|
119
|
+
*/
|
|
120
|
+
async function checkIfUserIsPopularInvestor(req, res, dependencies, config) {
|
|
121
|
+
const { db, logger } = dependencies;
|
|
122
|
+
const { userCid } = req.query;
|
|
123
|
+
|
|
124
|
+
if (!userCid) {
|
|
125
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Check for dev override impersonation
|
|
130
|
+
const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
|
|
131
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
132
|
+
const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
|
|
133
|
+
|
|
134
|
+
// Use effective CID (impersonated or actual) to check PI status
|
|
135
|
+
const rankEntry = await checkIfUserIsPI(db, effectiveCid, config, logger);
|
|
136
|
+
|
|
137
|
+
if (!rankEntry) {
|
|
138
|
+
return res.status(200).json({
|
|
139
|
+
isPopularInvestor: false,
|
|
140
|
+
rankingData: null,
|
|
141
|
+
isImpersonating: isImpersonating || false,
|
|
142
|
+
effectiveCid: effectiveCid
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if this is a dev override (pretendToBePI)
|
|
147
|
+
const isDevOverride = devOverride && devOverride.enabled && devOverride.pretendToBePI;
|
|
148
|
+
|
|
149
|
+
// Return ranking data
|
|
150
|
+
return res.status(200).json({
|
|
151
|
+
isPopularInvestor: true,
|
|
152
|
+
rankingData: {
|
|
153
|
+
cid: rankEntry.CustomerId,
|
|
154
|
+
username: rankEntry.UserName,
|
|
155
|
+
aum: rankEntry.AUMValue || 0,
|
|
156
|
+
copiers: rankEntry.Copiers || 0,
|
|
157
|
+
riskScore: rankEntry.RiskScore || 0,
|
|
158
|
+
gain: rankEntry.Gain || 0,
|
|
159
|
+
winRatio: rankEntry.WinRatio || 0,
|
|
160
|
+
trades: rankEntry.Trades || 0
|
|
161
|
+
},
|
|
162
|
+
isDevOverride: isDevOverride || false,
|
|
163
|
+
isImpersonating: isImpersonating || false,
|
|
164
|
+
effectiveCid: effectiveCid,
|
|
165
|
+
actualCid: Number(userCid)
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.log('ERROR', `[checkIfUserIsPopularInvestor] Error checking PI status for ${userCid}:`, error);
|
|
170
|
+
return res.status(500).json({ error: error.message });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
getUserVerification,
|
|
176
|
+
checkIfUserIsPopularInvestor
|
|
177
|
+
};
|
|
178
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview User Recommendations Helpers
|
|
3
|
+
* Handles personalized recommendations endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { readWithMigration } = require('../core/path_resolution_helpers');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /user/me/hedges (and /similar)
|
|
10
|
+
* Returns personalized recommendations calculated in Phase 3
|
|
11
|
+
*/
|
|
12
|
+
async function getUserRecommendations(req, res, dependencies, config, type = 'hedges') {
|
|
13
|
+
const { db, logger } = dependencies;
|
|
14
|
+
const { userCid } = req.query;
|
|
15
|
+
|
|
16
|
+
if (!userCid) {
|
|
17
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Try new path with migration
|
|
22
|
+
const result = await readWithMigration(
|
|
23
|
+
db,
|
|
24
|
+
'signedInUsers',
|
|
25
|
+
'recommendations',
|
|
26
|
+
{ cid: userCid },
|
|
27
|
+
{
|
|
28
|
+
isCollection: false,
|
|
29
|
+
dataType: 'recommendations',
|
|
30
|
+
config,
|
|
31
|
+
logger,
|
|
32
|
+
documentId: type
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (result && result.data) {
|
|
37
|
+
const recs = result.data[type] || result.data || [];
|
|
38
|
+
return res.status(200).json({
|
|
39
|
+
[type]: recs,
|
|
40
|
+
migrated: result.source === 'legacy'
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fallback: Recommendations may be stored in user doc
|
|
45
|
+
const userDoc = await db.collection(config.signedInUsersCollection || 'signedInUsers').doc(String(userCid)).get();
|
|
46
|
+
|
|
47
|
+
if (!userDoc.exists) {
|
|
48
|
+
return res.status(404).json({ error: "User not found" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = userDoc.data();
|
|
52
|
+
const recs = data.recommendations ? data.recommendations[type] : [];
|
|
53
|
+
|
|
54
|
+
return res.status(200).json({ [type]: recs || [] });
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.log('ERROR', `[getUserRecommendations] Error fetching recommendations for ${userCid}`, error);
|
|
58
|
+
return res.status(500).json({ error: error.message });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
getUserRecommendations
|
|
64
|
+
};
|
|
65
|
+
|