bulltrackers-module 1.0.421 → 1.0.423
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.
|
@@ -154,6 +154,67 @@ async function findLatestComputationDate(db, insightsCollection, resultsSub, com
|
|
|
154
154
|
return null; // No document found in the last maxDaysBack days
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Check if a signed-in user is also a Popular Investor
|
|
159
|
+
* Returns ranking entry if found, null otherwise
|
|
160
|
+
* Checks dev overrides first for pretendToBePI flag
|
|
161
|
+
*/
|
|
162
|
+
async function checkIfUserIsPI(db, userCid, config, logger = null) {
|
|
163
|
+
try {
|
|
164
|
+
// Check dev override first (for developer accounts)
|
|
165
|
+
const { getDevOverride } = require('./dev_helpers');
|
|
166
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
167
|
+
|
|
168
|
+
if (devOverride && devOverride.pretendToBePI) {
|
|
169
|
+
// Generate fake ranking entry for dev testing
|
|
170
|
+
const fakeRankEntry = {
|
|
171
|
+
CustomerId: Number(userCid),
|
|
172
|
+
UserName: 'Dev Test PI',
|
|
173
|
+
AUMValue: 500000 + Math.floor(Math.random() * 1000000), // Random AUM between 500k-1.5M
|
|
174
|
+
Copiers: 150 + Math.floor(Math.random() * 200), // Random copiers between 150-350
|
|
175
|
+
RiskScore: 3 + Math.floor(Math.random() * 3), // Random risk score 3-5
|
|
176
|
+
Gain: 25 + Math.floor(Math.random() * 50), // Random gain 25-75%
|
|
177
|
+
WinRatio: 50 + Math.floor(Math.random() * 20), // Random win ratio 50-70%
|
|
178
|
+
Trades: 500 + Math.floor(Math.random() * 1000) // Random trades 500-1500
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (logger && logger.log) {
|
|
182
|
+
logger.log('INFO', `[checkIfUserIsPI] DEV OVERRIDE: User ${userCid} pretending to be PI with fake ranking data`);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(`[checkIfUserIsPI] DEV OVERRIDE: User ${userCid} pretending to be PI with fake ranking data`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return fakeRankEntry;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Otherwise, check real rankings
|
|
191
|
+
const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
|
|
192
|
+
const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
|
|
193
|
+
|
|
194
|
+
if (!rankingsDate) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
|
|
199
|
+
const rankingsDoc = await rankingsRef.get();
|
|
200
|
+
|
|
201
|
+
if (!rankingsDoc.exists) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const rankingsData = rankingsDoc.data();
|
|
206
|
+
const rankingsItems = rankingsData.Items || [];
|
|
207
|
+
|
|
208
|
+
// Find user in rankings
|
|
209
|
+
const userRankEntry = rankingsItems.find(item => String(item.CustomerId) === String(userCid));
|
|
210
|
+
|
|
211
|
+
return userRankEntry || null;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('[checkIfUserIsPI] Error checking if user is PI:', error);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
157
218
|
/**
|
|
158
219
|
* Helper function to find the latest available date for Popular Investor rankings
|
|
159
220
|
* Searches backwards from today up to 30 days
|
|
@@ -546,12 +607,30 @@ async function getUserPortfolio(req, res, dependencies, config) {
|
|
|
546
607
|
return res.status(404).json({ error: "Portfolio data not found for this user" });
|
|
547
608
|
}
|
|
548
609
|
|
|
610
|
+
// Apply dev override to AggregatedMirrors if dev override is active
|
|
611
|
+
const { getDevOverride } = require('./dev_helpers');
|
|
612
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
613
|
+
if (devOverride && devOverride.enabled && devOverride.fakeCopiedPIs.length > 0 && portfolioData.AggregatedMirrors) {
|
|
614
|
+
logger.log('INFO', `[getUserPortfolio] Applying DEV OVERRIDE to AggregatedMirrors for user ${userCid}`);
|
|
615
|
+
|
|
616
|
+
// Replace AggregatedMirrors with fake ones from dev override
|
|
617
|
+
portfolioData.AggregatedMirrors = devOverride.fakeCopiedPIs.map(cid => ({
|
|
618
|
+
ParentCID: Number(cid),
|
|
619
|
+
ParentUsername: `PI-${cid}`, // Placeholder, will be resolved if needed
|
|
620
|
+
Invested: 0,
|
|
621
|
+
NetProfit: 0,
|
|
622
|
+
Value: 0,
|
|
623
|
+
PendingForClosure: false
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
|
|
549
627
|
return res.status(200).json({
|
|
550
628
|
portfolio: portfolioData,
|
|
551
629
|
date: dataDate,
|
|
552
630
|
isFallback: isFallback,
|
|
553
631
|
requestedDate: today,
|
|
554
|
-
userCid: String(userCid)
|
|
632
|
+
userCid: String(userCid),
|
|
633
|
+
devOverrideActive: devOverride && devOverride.enabled
|
|
555
634
|
});
|
|
556
635
|
|
|
557
636
|
} catch (error) {
|
|
@@ -710,6 +789,11 @@ async function getUserComputations(req, res, dependencies, config) {
|
|
|
710
789
|
return res.status(400).json({ error: "Please specify at least one computation name" });
|
|
711
790
|
}
|
|
712
791
|
|
|
792
|
+
// Check for dev override (for developer accounts)
|
|
793
|
+
const { getDevOverride } = require('./dev_helpers');
|
|
794
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
795
|
+
const isDevOverrideActive = devOverride && devOverride.enabled && devOverride.fakeCopiedPIs.length > 0;
|
|
796
|
+
|
|
713
797
|
let datesToCheck = [today];
|
|
714
798
|
|
|
715
799
|
// If mode is 'latest', try to find the latest available date for the first computation
|
|
@@ -777,7 +861,42 @@ async function getUserComputations(req, res, dependencies, config) {
|
|
|
777
861
|
const rawData = doc.data();
|
|
778
862
|
const data = tryDecompress(rawData);
|
|
779
863
|
// Filter by user CID - computation results are stored as { cid: result }
|
|
780
|
-
|
|
864
|
+
let userResult = data[String(userCid)];
|
|
865
|
+
|
|
866
|
+
// Apply dev override for computations that include copied PIs
|
|
867
|
+
if (isDevOverrideActive && (compName === 'SignedInUserProfileMetrics' || compName === 'SignedInUserCopiedPIs')) {
|
|
868
|
+
if (compName === 'SignedInUserCopiedPIs') {
|
|
869
|
+
// Override the copied PIs list
|
|
870
|
+
userResult = {
|
|
871
|
+
current: devOverride.fakeCopiedPIs,
|
|
872
|
+
past: [],
|
|
873
|
+
all: devOverride.fakeCopiedPIs
|
|
874
|
+
};
|
|
875
|
+
logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserCopiedPIs for user ${userCid}`);
|
|
876
|
+
} else if (compName === 'SignedInUserProfileMetrics' && userResult && userResult.copiedPIs) {
|
|
877
|
+
// Override the copiedPIs data in SignedInUserProfileMetrics
|
|
878
|
+
// We need to construct fake mirror data from the dev override PIs
|
|
879
|
+
const fakeMirrors = devOverride.fakeCopiedPIs.map(cid => ({
|
|
880
|
+
cid: Number(cid),
|
|
881
|
+
username: `PI-${cid}`, // Placeholder, will be resolved from rankings if available
|
|
882
|
+
invested: 0,
|
|
883
|
+
netProfit: 0,
|
|
884
|
+
value: 0,
|
|
885
|
+
pendingClosure: false,
|
|
886
|
+
isRanked: false
|
|
887
|
+
}));
|
|
888
|
+
|
|
889
|
+
userResult = {
|
|
890
|
+
...userResult,
|
|
891
|
+
copiedPIs: {
|
|
892
|
+
chartType: 'cards',
|
|
893
|
+
data: fakeMirrors
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
logger.log('INFO', `[getUserComputations] Applied DEV OVERRIDE to SignedInUserProfileMetrics.copiedPIs for user ${userCid}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
781
900
|
if (userResult) {
|
|
782
901
|
results[date][compName] = userResult;
|
|
783
902
|
}
|
|
@@ -809,7 +928,8 @@ async function getUserComputations(req, res, dependencies, config) {
|
|
|
809
928
|
computations: computationNames,
|
|
810
929
|
data: cleanedResults,
|
|
811
930
|
isFallback: isFallback,
|
|
812
|
-
requestedDate: today
|
|
931
|
+
requestedDate: today,
|
|
932
|
+
devOverrideActive: isDevOverrideActive
|
|
813
933
|
});
|
|
814
934
|
|
|
815
935
|
} catch (error) {
|
|
@@ -1619,6 +1739,609 @@ async function getPiProfile(req, res, dependencies, config) {
|
|
|
1619
1739
|
}
|
|
1620
1740
|
}
|
|
1621
1741
|
|
|
1742
|
+
/**
|
|
1743
|
+
* GET /user/me/is-popular-investor
|
|
1744
|
+
* Check if signed-in user is also a Popular Investor
|
|
1745
|
+
*/
|
|
1746
|
+
async function checkIfUserIsPopularInvestor(req, res, dependencies, config) {
|
|
1747
|
+
const { db, logger } = dependencies;
|
|
1748
|
+
const { userCid } = req.query;
|
|
1749
|
+
|
|
1750
|
+
if (!userCid) {
|
|
1751
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
try {
|
|
1755
|
+
const rankEntry = await checkIfUserIsPI(db, userCid, config, logger);
|
|
1756
|
+
|
|
1757
|
+
if (!rankEntry) {
|
|
1758
|
+
return res.status(200).json({
|
|
1759
|
+
isPopularInvestor: false,
|
|
1760
|
+
rankingData: null
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// Check if this is a dev override
|
|
1765
|
+
const { getDevOverride } = require('./dev_helpers');
|
|
1766
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
1767
|
+
const isDevOverride = devOverride && devOverride.pretendToBePI;
|
|
1768
|
+
|
|
1769
|
+
// Return ranking data
|
|
1770
|
+
return res.status(200).json({
|
|
1771
|
+
isPopularInvestor: true,
|
|
1772
|
+
rankingData: {
|
|
1773
|
+
cid: rankEntry.CustomerId,
|
|
1774
|
+
username: rankEntry.UserName,
|
|
1775
|
+
aum: rankEntry.AUMValue || 0,
|
|
1776
|
+
copiers: rankEntry.Copiers || 0,
|
|
1777
|
+
riskScore: rankEntry.RiskScore || 0,
|
|
1778
|
+
gain: rankEntry.Gain || 0,
|
|
1779
|
+
winRatio: rankEntry.WinRatio || 0,
|
|
1780
|
+
trades: rankEntry.Trades || 0
|
|
1781
|
+
},
|
|
1782
|
+
isDevOverride: isDevOverride || false
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
logger.log('ERROR', `[checkIfUserIsPopularInvestor] Error checking PI status for ${userCid}:`, error);
|
|
1787
|
+
return res.status(500).json({ error: error.message });
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Track profile page view for a Popular Investor
|
|
1793
|
+
* Stores view data for analytics
|
|
1794
|
+
*/
|
|
1795
|
+
async function trackProfileView(req, res, dependencies, config) {
|
|
1796
|
+
const { db, logger } = dependencies;
|
|
1797
|
+
const { piCid } = req.params;
|
|
1798
|
+
const { viewerCid, viewerType = 'anonymous' } = req.body; // viewerType: 'anonymous', 'signed_in', 'copier'
|
|
1799
|
+
|
|
1800
|
+
if (!piCid) {
|
|
1801
|
+
return res.status(400).json({ error: "Missing piCid" });
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
try {
|
|
1805
|
+
const profileViewsCollection = config.profileViewsCollection || 'profile_views';
|
|
1806
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1807
|
+
|
|
1808
|
+
// Create/update daily view document
|
|
1809
|
+
const viewDocId = `${piCid}_${today}`;
|
|
1810
|
+
const viewRef = db.collection(profileViewsCollection).doc(viewDocId);
|
|
1811
|
+
|
|
1812
|
+
// Get existing document to merge unique viewers properly
|
|
1813
|
+
const existingDoc = await viewRef.get();
|
|
1814
|
+
const existingData = existingDoc.exists ? existingDoc.data() : {};
|
|
1815
|
+
const existingUniqueViewers = Array.isArray(existingData.uniqueViewers) ? existingData.uniqueViewers : [];
|
|
1816
|
+
|
|
1817
|
+
// Add new viewer if provided and not already in list
|
|
1818
|
+
const updatedUniqueViewers = viewerCid && !existingUniqueViewers.includes(String(viewerCid))
|
|
1819
|
+
? [...existingUniqueViewers, String(viewerCid)]
|
|
1820
|
+
: existingUniqueViewers;
|
|
1821
|
+
|
|
1822
|
+
const viewData = {
|
|
1823
|
+
piCid: Number(piCid),
|
|
1824
|
+
date: today,
|
|
1825
|
+
totalViews: FieldValue.increment(1),
|
|
1826
|
+
uniqueViewers: updatedUniqueViewers, // Store as array, not using arrayUnion to avoid duplicates
|
|
1827
|
+
lastUpdated: FieldValue.serverTimestamp()
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
// Set with merge to update if exists
|
|
1831
|
+
await viewRef.set(viewData, { merge: true });
|
|
1832
|
+
|
|
1833
|
+
// Also track individual view for detailed analytics (optional, lightweight)
|
|
1834
|
+
if (viewerCid) {
|
|
1835
|
+
const individualViewId = `${piCid}_${viewerCid}_${Date.now()}`;
|
|
1836
|
+
await db.collection(profileViewsCollection)
|
|
1837
|
+
.doc('individual_views')
|
|
1838
|
+
.collection('views')
|
|
1839
|
+
.doc(individualViewId)
|
|
1840
|
+
.set({
|
|
1841
|
+
piCid: Number(piCid),
|
|
1842
|
+
viewerCid: Number(viewerCid),
|
|
1843
|
+
viewerType: viewerType,
|
|
1844
|
+
viewedAt: FieldValue.serverTimestamp(),
|
|
1845
|
+
date: today
|
|
1846
|
+
}, { merge: true });
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
return res.status(200).json({ success: true, message: "View tracked" });
|
|
1850
|
+
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
logger.log('ERROR', `[trackProfileView] Error tracking view for PI ${piCid}:`, error);
|
|
1853
|
+
// Don't fail the request if tracking fails
|
|
1854
|
+
return res.status(200).json({ success: false, message: "View tracking failed but request succeeded" });
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* Generate sample PI personalized metrics data for dev testing
|
|
1860
|
+
* Creates realistic sample data that matches the computation structure 1:1
|
|
1861
|
+
*/
|
|
1862
|
+
function generateSamplePIPersonalizedMetrics(userCid, rankEntry) {
|
|
1863
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1864
|
+
const yesterday = new Date();
|
|
1865
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
1866
|
+
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
1867
|
+
|
|
1868
|
+
// Generate sample base metrics (from PopularInvestorProfileMetrics)
|
|
1869
|
+
const baseMetrics = {
|
|
1870
|
+
socialEngagement: {
|
|
1871
|
+
chartType: 'line',
|
|
1872
|
+
data: Array.from({ length: 30 }, (_, i) => {
|
|
1873
|
+
const date = new Date();
|
|
1874
|
+
date.setDate(date.getDate() - (29 - i));
|
|
1875
|
+
return {
|
|
1876
|
+
date: date.toISOString().split('T')[0],
|
|
1877
|
+
likes: Math.floor(Math.random() * 50) + 10,
|
|
1878
|
+
comments: Math.floor(Math.random() * 20) + 5
|
|
1879
|
+
};
|
|
1880
|
+
})
|
|
1881
|
+
},
|
|
1882
|
+
profitablePositions: {
|
|
1883
|
+
chartType: 'bar',
|
|
1884
|
+
data: Array.from({ length: 30 }, (_, i) => {
|
|
1885
|
+
const date = new Date();
|
|
1886
|
+
date.setDate(date.getDate() - (29 - i));
|
|
1887
|
+
return {
|
|
1888
|
+
date: date.toISOString().split('T')[0],
|
|
1889
|
+
profitableCount: Math.floor(Math.random() * 10) + 5,
|
|
1890
|
+
totalCount: Math.floor(Math.random() * 15) + 10
|
|
1891
|
+
};
|
|
1892
|
+
})
|
|
1893
|
+
},
|
|
1894
|
+
topWinningPositions: {
|
|
1895
|
+
chartType: 'table',
|
|
1896
|
+
data: Array.from({ length: 10 }, (_, i) => ({
|
|
1897
|
+
instrumentId: 1000 + i,
|
|
1898
|
+
ticker: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'NFLX', 'AMD', 'INTC'][i],
|
|
1899
|
+
netProfit: Math.floor(Math.random() * 100) + 20,
|
|
1900
|
+
invested: Math.floor(Math.random() * 5000) + 1000,
|
|
1901
|
+
value: Math.floor(Math.random() * 6000) + 1000,
|
|
1902
|
+
isCurrent: Math.random() > 0.5,
|
|
1903
|
+
closeDate: Math.random() > 0.5 ? new Date().toISOString() : null
|
|
1904
|
+
}))
|
|
1905
|
+
},
|
|
1906
|
+
sectorPerformance: {
|
|
1907
|
+
bestSector: 'Technology',
|
|
1908
|
+
worstSector: 'Energy',
|
|
1909
|
+
bestSectorProfit: 15.5,
|
|
1910
|
+
worstSectorProfit: -3.2
|
|
1911
|
+
},
|
|
1912
|
+
sectorExposure: {
|
|
1913
|
+
chartType: 'pie',
|
|
1914
|
+
data: {
|
|
1915
|
+
'Technology': 35.5,
|
|
1916
|
+
'Healthcare': 20.3,
|
|
1917
|
+
'Finance': 15.2,
|
|
1918
|
+
'Energy': 10.1,
|
|
1919
|
+
'Consumer': 18.9
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
assetExposure: {
|
|
1923
|
+
chartType: 'pie',
|
|
1924
|
+
data: {
|
|
1925
|
+
'AAPL': 12.5,
|
|
1926
|
+
'MSFT': 10.3,
|
|
1927
|
+
'GOOGL': 8.7,
|
|
1928
|
+
'AMZN': 7.2,
|
|
1929
|
+
'TSLA': 6.1
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
portfolioSummary: {
|
|
1933
|
+
totalInvested: 850000,
|
|
1934
|
+
totalProfit: 125000,
|
|
1935
|
+
profitPercent: 14.7
|
|
1936
|
+
},
|
|
1937
|
+
rankingsData: {
|
|
1938
|
+
aum: rankEntry.AUMValue || 0,
|
|
1939
|
+
riskScore: rankEntry.RiskScore || 0,
|
|
1940
|
+
gain: rankEntry.Gain || 0,
|
|
1941
|
+
copiers: rankEntry.Copiers || 0,
|
|
1942
|
+
winRatio: rankEntry.WinRatio || 0,
|
|
1943
|
+
trades: rankEntry.Trades || 0
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
// Generate sample review metrics
|
|
1948
|
+
const reviewMetricsOverTime = Array.from({ length: 30 }, (_, i) => {
|
|
1949
|
+
const date = new Date();
|
|
1950
|
+
date.setDate(date.getDate() - (29 - i));
|
|
1951
|
+
return {
|
|
1952
|
+
date: date.toISOString().split('T')[0],
|
|
1953
|
+
averageRating: Number((3.5 + Math.random() * 1.5).toFixed(2)),
|
|
1954
|
+
count: Math.floor(Math.random() * 5) + 1
|
|
1955
|
+
};
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
const totalReviews = reviewMetricsOverTime.reduce((sum, r) => sum + r.count, 0);
|
|
1959
|
+
const avgRating = reviewMetricsOverTime.reduce((sum, r) => sum + (r.averageRating * r.count), 0) / totalReviews;
|
|
1960
|
+
|
|
1961
|
+
// Generate sample similar PIs
|
|
1962
|
+
const similarPIs = Array.from({ length: 5 }, (_, i) => ({
|
|
1963
|
+
cid: 1000000 + i,
|
|
1964
|
+
username: `SimilarPI${i + 1}`,
|
|
1965
|
+
similarityScore: Number((85 - (i * 5)).toFixed(2)),
|
|
1966
|
+
aum: Math.floor(Math.random() * 500000) + 300000,
|
|
1967
|
+
copiers: Math.floor(Math.random() * 200) + 100,
|
|
1968
|
+
riskScore: Math.floor(Math.random() * 3) + 2,
|
|
1969
|
+
gain: Number((20 + Math.random() * 30).toFixed(2))
|
|
1970
|
+
}));
|
|
1971
|
+
|
|
1972
|
+
// Calculate leaderboard position (sample)
|
|
1973
|
+
const totalPIs = 1000;
|
|
1974
|
+
const currentRank = Math.floor(Math.random() * 200) + 50; // Rank 50-250
|
|
1975
|
+
const percentile = Number((100 - ((currentRank / totalPIs) * 100)).toFixed(2));
|
|
1976
|
+
|
|
1977
|
+
// Generate sample asset investments
|
|
1978
|
+
const topAssets = Array.from({ length: 10 }, (_, i) => ({
|
|
1979
|
+
instrumentId: 1000 + i,
|
|
1980
|
+
ticker: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'NFLX', 'AMD', 'INTC'][i],
|
|
1981
|
+
aum: Math.floor(Math.random() * 100000) + 50000,
|
|
1982
|
+
percentage: Number((Math.random() * 15 + 5).toFixed(2))
|
|
1983
|
+
}));
|
|
1984
|
+
|
|
1985
|
+
// Generate sample profile views
|
|
1986
|
+
const profileViewsOverTime = Array.from({ length: 30 }, (_, i) => {
|
|
1987
|
+
const date = new Date();
|
|
1988
|
+
date.setDate(date.getDate() - (29 - i));
|
|
1989
|
+
return {
|
|
1990
|
+
date: date.toISOString().split('T')[0],
|
|
1991
|
+
views: Math.floor(Math.random() * 50) + 10,
|
|
1992
|
+
uniqueViews: Math.floor(Math.random() * 30) + 5
|
|
1993
|
+
};
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
const totalViews = profileViewsOverTime.reduce((sum, v) => sum + v.views, 0);
|
|
1997
|
+
const totalUniqueViews = new Set(profileViewsOverTime.flatMap(v => Array.from({ length: v.uniqueViews }, (_, i) => `viewer_${i}`))).size;
|
|
1998
|
+
const averageDailyViews = Number((totalViews / 30).toFixed(2));
|
|
1999
|
+
|
|
2000
|
+
// Calculate copier growth
|
|
2001
|
+
const copiersToday = rankEntry.Copiers || 250;
|
|
2002
|
+
const copiersYesterday = copiersToday - Math.floor(Math.random() * 20) + 5; // Random change
|
|
2003
|
+
const diff = copiersToday - copiersYesterday;
|
|
2004
|
+
const growthRate = copiersYesterday > 0 ? Number(((diff / copiersYesterday) * 100).toFixed(2)) : 0;
|
|
2005
|
+
const trend = diff > 0 ? 'increasing' : diff < 0 ? 'decreasing' : 'stable';
|
|
2006
|
+
|
|
2007
|
+
// Calculate engagement score
|
|
2008
|
+
const reviewScore = avgRating * 20; // 0-100 scale
|
|
2009
|
+
const viewScore = Math.min(100, (averageDailyViews / 10) * 100);
|
|
2010
|
+
const copierScore = Math.min(100, (copiersToday / 1000) * 100);
|
|
2011
|
+
const socialScore = Math.min(100, (baseMetrics.socialEngagement.data.length / 10) * 100);
|
|
2012
|
+
const engagementScore = (reviewScore * 0.3 + viewScore * 0.2 + copierScore * 0.3 + socialScore * 0.2);
|
|
2013
|
+
|
|
2014
|
+
return {
|
|
2015
|
+
baseMetrics: baseMetrics,
|
|
2016
|
+
reviewMetrics: {
|
|
2017
|
+
overTime: reviewMetricsOverTime,
|
|
2018
|
+
currentStats: {
|
|
2019
|
+
averageRating: Number(avgRating.toFixed(2)),
|
|
2020
|
+
totalReviews: totalReviews,
|
|
2021
|
+
distribution: {
|
|
2022
|
+
1: Math.floor(totalReviews * 0.1),
|
|
2023
|
+
2: Math.floor(totalReviews * 0.15),
|
|
2024
|
+
3: Math.floor(totalReviews * 0.25),
|
|
2025
|
+
4: Math.floor(totalReviews * 0.3),
|
|
2026
|
+
5: Math.floor(totalReviews * 0.2)
|
|
2027
|
+
}
|
|
2028
|
+
},
|
|
2029
|
+
sentimentTrend: []
|
|
2030
|
+
},
|
|
2031
|
+
similarPIs: similarPIs,
|
|
2032
|
+
leaderboardPosition: {
|
|
2033
|
+
currentRank: currentRank,
|
|
2034
|
+
totalPIs: totalPIs,
|
|
2035
|
+
percentile: percentile,
|
|
2036
|
+
rankByAUM: currentRank + Math.floor(Math.random() * 50) - 25,
|
|
2037
|
+
rankByCopiers: currentRank
|
|
2038
|
+
},
|
|
2039
|
+
assetInvestments: {
|
|
2040
|
+
byAUM: topAssets.reduce((acc, asset) => {
|
|
2041
|
+
acc[asset.instrumentId] = asset.aum;
|
|
2042
|
+
return acc;
|
|
2043
|
+
}, {}),
|
|
2044
|
+
topAssets: topAssets
|
|
2045
|
+
},
|
|
2046
|
+
profileViews: {
|
|
2047
|
+
overTime: profileViewsOverTime,
|
|
2048
|
+
totalViews: totalViews,
|
|
2049
|
+
totalUniqueViews: totalUniqueViews,
|
|
2050
|
+
averageDailyViews: averageDailyViews
|
|
2051
|
+
},
|
|
2052
|
+
copierGrowth: {
|
|
2053
|
+
overTime: [
|
|
2054
|
+
{ date: yesterdayStr, copiers: copiersYesterday },
|
|
2055
|
+
{ date: today, copiers: copiersToday }
|
|
2056
|
+
],
|
|
2057
|
+
growthRate: growthRate,
|
|
2058
|
+
diff: diff,
|
|
2059
|
+
trend: trend
|
|
2060
|
+
},
|
|
2061
|
+
engagementScore: {
|
|
2062
|
+
current: Number(engagementScore.toFixed(2)),
|
|
2063
|
+
components: {
|
|
2064
|
+
reviewScore: Number(reviewScore.toFixed(2)),
|
|
2065
|
+
viewScore: Number(viewScore.toFixed(2)),
|
|
2066
|
+
copierScore: Number(copierScore.toFixed(2)),
|
|
2067
|
+
socialScore: Number(socialScore.toFixed(2))
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* GET /user/me/pi-personalized-metrics
|
|
2075
|
+
* Get personalized metrics for signed-in user who is a Popular Investor
|
|
2076
|
+
* Includes review metrics, page views, similar PIs, leaderboard position, etc.
|
|
2077
|
+
*/
|
|
2078
|
+
async function getSignedInUserPIPersonalizedMetrics(req, res, dependencies, config) {
|
|
2079
|
+
const { db, logger } = dependencies;
|
|
2080
|
+
const { userCid } = req.query;
|
|
2081
|
+
|
|
2082
|
+
if (!userCid) {
|
|
2083
|
+
return res.status(400).json({ error: "Missing userCid" });
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
try {
|
|
2087
|
+
// First, verify user is a PI (checks dev override)
|
|
2088
|
+
const rankEntry = await checkIfUserIsPI(db, userCid, config, logger);
|
|
2089
|
+
if (!rankEntry) {
|
|
2090
|
+
return res.status(404).json({
|
|
2091
|
+
error: "Not a Popular Investor",
|
|
2092
|
+
message: "This endpoint is only available for users who are Popular Investors"
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Check if this is a dev override
|
|
2097
|
+
const { getDevOverride } = require('./dev_helpers');
|
|
2098
|
+
const devOverride = await getDevOverride(db, userCid, config, logger);
|
|
2099
|
+
const isDevOverride = devOverride && devOverride.pretendToBePI;
|
|
2100
|
+
|
|
2101
|
+
// If dev override, generate sample data
|
|
2102
|
+
if (isDevOverride) {
|
|
2103
|
+
logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] DEV OVERRIDE: Generating sample PI metrics for user ${userCid}`);
|
|
2104
|
+
const sampleData = generateSamplePIPersonalizedMetrics(userCid, rankEntry);
|
|
2105
|
+
const today = new Date().toISOString().split('T')[0];
|
|
2106
|
+
|
|
2107
|
+
return res.status(200).json({
|
|
2108
|
+
status: 'success',
|
|
2109
|
+
userCid: String(userCid),
|
|
2110
|
+
data: sampleData,
|
|
2111
|
+
dataDate: today,
|
|
2112
|
+
requestedDate: today,
|
|
2113
|
+
isFallback: false,
|
|
2114
|
+
isDevOverride: true
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
|
|
2119
|
+
const resultsSub = config.resultsSubcollection || 'results';
|
|
2120
|
+
const compsSub = config.computationsSubcollection || 'computations';
|
|
2121
|
+
const category = 'popular-investor';
|
|
2122
|
+
const computationName = 'SignedInUserPIPersonalizedMetrics';
|
|
2123
|
+
const today = new Date().toISOString().split('T')[0];
|
|
2124
|
+
|
|
2125
|
+
// Try to find computation with user-specific fallback logic
|
|
2126
|
+
let foundDate = null;
|
|
2127
|
+
let metricsData = null;
|
|
2128
|
+
let checkedDates = [];
|
|
2129
|
+
|
|
2130
|
+
// Step 1: Try today, then look back 30 days for computation document
|
|
2131
|
+
const latestComputationDate = await findLatestComputationDate(
|
|
2132
|
+
db,
|
|
2133
|
+
insightsCollection,
|
|
2134
|
+
resultsSub,
|
|
2135
|
+
compsSub,
|
|
2136
|
+
category,
|
|
2137
|
+
computationName,
|
|
2138
|
+
null, // Don't check for specific user yet
|
|
2139
|
+
30
|
|
2140
|
+
);
|
|
2141
|
+
|
|
2142
|
+
if (!latestComputationDate) {
|
|
2143
|
+
// No computation exists at all - will fallback to frontend
|
|
2144
|
+
logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] No computation document found, will use frontend fallback`);
|
|
2145
|
+
return res.status(200).json({
|
|
2146
|
+
status: 'fallback',
|
|
2147
|
+
message: 'Computation not available, use frontend fallback',
|
|
2148
|
+
data: null,
|
|
2149
|
+
useFrontendFallback: true
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Step 2: Check if user exists in latest computation, if not, look back 7 days
|
|
2154
|
+
const latestDateObj = new Date(latestComputationDate + 'T00:00:00Z');
|
|
2155
|
+
|
|
2156
|
+
for (let daysBack = 0; daysBack <= 7; daysBack++) {
|
|
2157
|
+
const checkDate = new Date(latestDateObj);
|
|
2158
|
+
checkDate.setUTCDate(latestDateObj.getUTCDate() - daysBack);
|
|
2159
|
+
const dateStr = checkDate.toISOString().split('T')[0];
|
|
2160
|
+
checkedDates.push(dateStr);
|
|
2161
|
+
|
|
2162
|
+
const computationRef = db.collection(insightsCollection)
|
|
2163
|
+
.doc(dateStr)
|
|
2164
|
+
.collection(resultsSub)
|
|
2165
|
+
.doc(category)
|
|
2166
|
+
.collection(compsSub)
|
|
2167
|
+
.doc(computationName);
|
|
2168
|
+
|
|
2169
|
+
const computationDoc = await computationRef.get();
|
|
2170
|
+
|
|
2171
|
+
if (computationDoc.exists) {
|
|
2172
|
+
const rawData = computationDoc.data();
|
|
2173
|
+
let computationData = tryDecompress(rawData);
|
|
2174
|
+
|
|
2175
|
+
if (typeof computationData === 'string') {
|
|
2176
|
+
try {
|
|
2177
|
+
computationData = JSON.parse(computationData);
|
|
2178
|
+
} catch (e) {
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// Check if user exists in this computation
|
|
2184
|
+
if (computationData && typeof computationData === 'object' && !Array.isArray(computationData)) {
|
|
2185
|
+
const userData = computationData[String(userCid)];
|
|
2186
|
+
if (userData) {
|
|
2187
|
+
foundDate = dateStr;
|
|
2188
|
+
metricsData = userData;
|
|
2189
|
+
logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] Found user ${userCid} in computation date ${dateStr}`);
|
|
2190
|
+
break;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Step 3: If user not found in any computation, return fallback flag
|
|
2197
|
+
if (!foundDate || !metricsData) {
|
|
2198
|
+
logger.log('INFO', `[getSignedInUserPIPersonalizedMetrics] User ${userCid} not found in any computation, will use frontend fallback`);
|
|
2199
|
+
return res.status(200).json({
|
|
2200
|
+
status: 'fallback',
|
|
2201
|
+
message: 'User not found in computation, use frontend fallback',
|
|
2202
|
+
data: null,
|
|
2203
|
+
useFrontendFallback: true,
|
|
2204
|
+
checkedDates: checkedDates
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// Step 4: Enhance with review metrics and page views from Firestore
|
|
2209
|
+
// These require cross-collection queries that aren't in computation
|
|
2210
|
+
|
|
2211
|
+
// Fetch review metrics over time
|
|
2212
|
+
const reviewsCollection = config.reviewsCollection || 'pi_reviews';
|
|
2213
|
+
const reviewsSnapshot = await db.collection(reviewsCollection)
|
|
2214
|
+
.where('piCid', '==', Number(userCid))
|
|
2215
|
+
.orderBy('createdAt', 'desc')
|
|
2216
|
+
.limit(1000) // Get enough for time series
|
|
2217
|
+
.get();
|
|
2218
|
+
|
|
2219
|
+
const reviewTimeMap = new Map();
|
|
2220
|
+
let totalReviews = 0;
|
|
2221
|
+
let totalRating = 0;
|
|
2222
|
+
const ratingDistribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
2223
|
+
|
|
2224
|
+
reviewsSnapshot.forEach(doc => {
|
|
2225
|
+
const review = doc.data();
|
|
2226
|
+
const rating = review.rating || 0;
|
|
2227
|
+
totalReviews++;
|
|
2228
|
+
totalRating += rating;
|
|
2229
|
+
ratingDistribution[rating] = (ratingDistribution[rating] || 0) + 1;
|
|
2230
|
+
|
|
2231
|
+
if (review.createdAt) {
|
|
2232
|
+
const reviewDate = review.createdAt.toDate ? review.createdAt.toDate().toISOString().split('T')[0] : review.createdAt;
|
|
2233
|
+
const existing = reviewTimeMap.get(reviewDate) || { date: reviewDate, count: 0, totalRating: 0 };
|
|
2234
|
+
existing.count++;
|
|
2235
|
+
existing.totalRating += rating;
|
|
2236
|
+
reviewTimeMap.set(reviewDate, existing);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
const reviewMetricsOverTime = Array.from(reviewTimeMap.values())
|
|
2241
|
+
.map(entry => ({
|
|
2242
|
+
date: entry.date,
|
|
2243
|
+
averageRating: entry.count > 0 ? Number((entry.totalRating / entry.count).toFixed(2)) : 0,
|
|
2244
|
+
count: entry.count
|
|
2245
|
+
}))
|
|
2246
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
2247
|
+
|
|
2248
|
+
metricsData.reviewMetrics = {
|
|
2249
|
+
overTime: reviewMetricsOverTime,
|
|
2250
|
+
currentStats: {
|
|
2251
|
+
averageRating: totalReviews > 0 ? Number((totalRating / totalReviews).toFixed(2)) : 0,
|
|
2252
|
+
totalReviews: totalReviews,
|
|
2253
|
+
distribution: ratingDistribution
|
|
2254
|
+
},
|
|
2255
|
+
sentimentTrend: [] // Could analyze review text sentiment if needed
|
|
2256
|
+
};
|
|
2257
|
+
|
|
2258
|
+
// Fetch profile views over time
|
|
2259
|
+
const profileViewsCollection = config.profileViewsCollection || 'profile_views';
|
|
2260
|
+
// Query without orderBy first, then sort in memory (to avoid index requirement)
|
|
2261
|
+
const viewsSnapshot = await db.collection(profileViewsCollection)
|
|
2262
|
+
.where('piCid', '==', Number(userCid))
|
|
2263
|
+
.limit(90) // Last 90 days worth of documents
|
|
2264
|
+
.get();
|
|
2265
|
+
|
|
2266
|
+
const viewsOverTime = [];
|
|
2267
|
+
let totalViews = 0;
|
|
2268
|
+
let totalUniqueViews = 0;
|
|
2269
|
+
const uniqueViewersSet = new Set();
|
|
2270
|
+
const viewsByDate = new Map();
|
|
2271
|
+
|
|
2272
|
+
viewsSnapshot.forEach(doc => {
|
|
2273
|
+
const viewData = doc.data();
|
|
2274
|
+
const date = viewData.date;
|
|
2275
|
+
if (!date) return;
|
|
2276
|
+
|
|
2277
|
+
const views = typeof viewData.totalViews === 'number' ? viewData.totalViews : 0;
|
|
2278
|
+
const uniqueViewers = Array.isArray(viewData.uniqueViewers) ? viewData.uniqueViewers : [];
|
|
2279
|
+
|
|
2280
|
+
// Aggregate by date (in case there are multiple docs per date)
|
|
2281
|
+
const existing = viewsByDate.get(date) || { date, views: 0, uniqueViewers: [] };
|
|
2282
|
+
existing.views += views;
|
|
2283
|
+
existing.uniqueViewers = [...new Set([...existing.uniqueViewers, ...uniqueViewers])];
|
|
2284
|
+
viewsByDate.set(date, existing);
|
|
2285
|
+
});
|
|
2286
|
+
|
|
2287
|
+
// Convert map to array and calculate totals
|
|
2288
|
+
for (const [date, data] of viewsByDate.entries()) {
|
|
2289
|
+
totalViews += data.views;
|
|
2290
|
+
data.uniqueViewers.forEach(v => uniqueViewersSet.add(v));
|
|
2291
|
+
|
|
2292
|
+
viewsOverTime.push({
|
|
2293
|
+
date: date,
|
|
2294
|
+
views: data.views,
|
|
2295
|
+
uniqueViews: data.uniqueViewers.length
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
totalUniqueViews = uniqueViewersSet.size;
|
|
2300
|
+
const averageDailyViews = viewsOverTime.length > 0 ? Number((totalViews / viewsOverTime.length).toFixed(2)) : 0;
|
|
2301
|
+
|
|
2302
|
+
metricsData.profileViews = {
|
|
2303
|
+
overTime: viewsOverTime.sort((a, b) => a.date.localeCompare(b.date)),
|
|
2304
|
+
totalViews: totalViews,
|
|
2305
|
+
totalUniqueViews: totalUniqueViews,
|
|
2306
|
+
averageDailyViews: averageDailyViews
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
// Calculate engagement score
|
|
2310
|
+
const reviewScore = metricsData.reviewMetrics.currentStats.averageRating * 20; // 0-100 scale
|
|
2311
|
+
const viewScore = Math.min(100, (averageDailyViews / 10) * 100); // Cap at 100 for 10+ daily views
|
|
2312
|
+
const copierScore = rankEntry.Copiers ? Math.min(100, (rankEntry.Copiers / 1000) * 100) : 0; // Cap at 100 for 1000+ copiers
|
|
2313
|
+
const socialScore = metricsData.baseMetrics?.socialEngagement?.data?.length > 0 ? Math.min(100, (metricsData.baseMetrics.socialEngagement.data.length / 10) * 100) : 0;
|
|
2314
|
+
|
|
2315
|
+
const engagementScore = (reviewScore * 0.3 + viewScore * 0.2 + copierScore * 0.3 + socialScore * 0.2);
|
|
2316
|
+
|
|
2317
|
+
metricsData.engagementScore = {
|
|
2318
|
+
current: Number(engagementScore.toFixed(2)),
|
|
2319
|
+
components: {
|
|
2320
|
+
reviewScore: Number(reviewScore.toFixed(2)),
|
|
2321
|
+
viewScore: Number(viewScore.toFixed(2)),
|
|
2322
|
+
copierScore: Number(copierScore.toFixed(2)),
|
|
2323
|
+
socialScore: Number(socialScore.toFixed(2))
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
|
|
2327
|
+
logger.log('SUCCESS', `[getSignedInUserPIPersonalizedMetrics] Returning personalized metrics for user ${userCid} from date ${foundDate}`);
|
|
2328
|
+
|
|
2329
|
+
return res.status(200).json({
|
|
2330
|
+
status: 'success',
|
|
2331
|
+
userCid: String(userCid),
|
|
2332
|
+
data: metricsData,
|
|
2333
|
+
dataDate: foundDate,
|
|
2334
|
+
requestedDate: today,
|
|
2335
|
+
isFallback: foundDate !== today,
|
|
2336
|
+
daysBackFromLatest: checkedDates.indexOf(foundDate)
|
|
2337
|
+
});
|
|
2338
|
+
|
|
2339
|
+
} catch (error) {
|
|
2340
|
+
logger.log('ERROR', `[getSignedInUserPIPersonalizedMetrics] Error fetching personalized metrics for ${userCid}:`, error);
|
|
2341
|
+
return res.status(500).json({ error: error.message });
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
1622
2345
|
module.exports = {
|
|
1623
2346
|
getPiAnalytics,
|
|
1624
2347
|
getUserRecommendations,
|
|
@@ -1635,5 +2358,8 @@ module.exports = {
|
|
|
1635
2358
|
requestPiAddition,
|
|
1636
2359
|
getWatchlistTriggerCounts,
|
|
1637
2360
|
checkPisInRankings,
|
|
1638
|
-
getPiProfile
|
|
2361
|
+
getPiProfile,
|
|
2362
|
+
checkIfUserIsPopularInvestor,
|
|
2363
|
+
trackProfileView,
|
|
2364
|
+
getSignedInUserPIPersonalizedMetrics
|
|
1639
2365
|
};
|
|
@@ -21,7 +21,7 @@ function isDeveloperAccount(userCid) {
|
|
|
21
21
|
/**
|
|
22
22
|
* Get developer override data for a user
|
|
23
23
|
* Auto-creates default document if it doesn't exist (for developer accounts only)
|
|
24
|
-
* Returns null if not a
|
|
24
|
+
* Returns null if not a developerg,
|
|
25
25
|
*/
|
|
26
26
|
async function getDevOverride(db, userCid, config, logger = null) {
|
|
27
27
|
if (!isDeveloperAccount(userCid)) {
|
|
@@ -39,6 +39,7 @@ async function getDevOverride(db, userCid, config, logger = null) {
|
|
|
39
39
|
userCid: Number(userCid),
|
|
40
40
|
enabled: false,
|
|
41
41
|
fakeCopiedPIs: [],
|
|
42
|
+
pretendToBePI: false,
|
|
42
43
|
createdAt: FieldValue.serverTimestamp(),
|
|
43
44
|
lastUpdated: FieldValue.serverTimestamp()
|
|
44
45
|
};
|
|
@@ -56,6 +57,7 @@ async function getDevOverride(db, userCid, config, logger = null) {
|
|
|
56
57
|
return {
|
|
57
58
|
enabled: false,
|
|
58
59
|
fakeCopiedPIs: [],
|
|
60
|
+
pretendToBePI: false,
|
|
59
61
|
lastUpdated: null,
|
|
60
62
|
wasAutoCreated: true
|
|
61
63
|
};
|
|
@@ -67,6 +69,7 @@ async function getDevOverride(db, userCid, config, logger = null) {
|
|
|
67
69
|
return {
|
|
68
70
|
enabled: data.enabled === true,
|
|
69
71
|
fakeCopiedPIs: data.fakeCopiedPIs || [],
|
|
72
|
+
pretendToBePI: data.pretendToBePI === true,
|
|
70
73
|
lastUpdated: data.lastUpdated,
|
|
71
74
|
wasAutoCreated: false
|
|
72
75
|
};
|
|
@@ -151,7 +154,7 @@ async function hasUserCopiedWithDevOverride(db, userCid, piCid, config, logger)
|
|
|
151
154
|
*/
|
|
152
155
|
async function setDevOverride(req, res, dependencies, config) {
|
|
153
156
|
const { db, logger } = dependencies;
|
|
154
|
-
const { userCid, enabled, fakeCopiedPIs } = req.body;
|
|
157
|
+
const { userCid, enabled, fakeCopiedPIs, pretendToBePI } = req.body;
|
|
155
158
|
|
|
156
159
|
if (!userCid) {
|
|
157
160
|
return res.status(400).json({ error: "Missing userCid" });
|
|
@@ -166,7 +169,7 @@ async function setDevOverride(req, res, dependencies, config) {
|
|
|
166
169
|
});
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
// Validate fakeCopiedPIs
|
|
172
|
+
// Validate fakeCopiedPIs (only required if enabled is true)
|
|
170
173
|
if (enabled && (!Array.isArray(fakeCopiedPIs) || fakeCopiedPIs.length === 0)) {
|
|
171
174
|
return res.status(400).json({
|
|
172
175
|
error: "Invalid request",
|
|
@@ -181,16 +184,21 @@ async function setDevOverride(req, res, dependencies, config) {
|
|
|
181
184
|
const devOverridesCollection = config.devOverridesCollection || 'dev_overrides';
|
|
182
185
|
const overrideRef = db.collection(devOverridesCollection).doc(String(userCid));
|
|
183
186
|
|
|
187
|
+
// Get existing override to preserve values not being updated
|
|
188
|
+
const existingDoc = await overrideRef.get();
|
|
189
|
+
const existingData = existingDoc.exists ? existingDoc.data() : {};
|
|
190
|
+
|
|
184
191
|
const overrideData = {
|
|
185
192
|
userCid: Number(userCid),
|
|
186
|
-
enabled: enabled === true,
|
|
187
|
-
fakeCopiedPIs: validatedPIs,
|
|
193
|
+
enabled: enabled !== undefined ? (enabled === true) : (existingData.enabled || false),
|
|
194
|
+
fakeCopiedPIs: enabled !== undefined ? validatedPIs : (existingData.fakeCopiedPIs || []),
|
|
195
|
+
pretendToBePI: pretendToBePI !== undefined ? (pretendToBePI === true) : (existingData.pretendToBePI || false),
|
|
188
196
|
lastUpdated: FieldValue.serverTimestamp()
|
|
189
197
|
};
|
|
190
198
|
|
|
191
199
|
await overrideRef.set(overrideData, { merge: true });
|
|
192
200
|
|
|
193
|
-
logger.log('SUCCESS', `[setDevOverride] Updated dev override for user ${userCid}: enabled=${enabled}, ${validatedPIs.length} fake PIs`);
|
|
201
|
+
logger.log('SUCCESS', `[setDevOverride] Updated dev override for user ${userCid}: enabled=${overrideData.enabled}, pretendToBePI=${overrideData.pretendToBePI}, ${validatedPIs.length} fake PIs`);
|
|
194
202
|
|
|
195
203
|
return res.status(200).json({
|
|
196
204
|
success: true,
|
|
@@ -239,6 +247,7 @@ async function getDevOverrideStatus(req, res, dependencies, config) {
|
|
|
239
247
|
return res.status(200).json({
|
|
240
248
|
enabled: devOverride.enabled,
|
|
241
249
|
fakeCopiedPIs: devOverride.fakeCopiedPIs,
|
|
250
|
+
pretendToBePI: devOverride.pretendToBePI || false,
|
|
242
251
|
lastUpdated: devOverride.lastUpdated,
|
|
243
252
|
wasAutoCreated: devOverride.wasAutoCreated || false
|
|
244
253
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const express = require('express');
|
|
6
6
|
const { submitReview, getReviews, getUserReview, checkReviewEligibility } = require('./helpers/review_helpers');
|
|
7
|
-
const { getPiAnalytics, getUserRecommendations, getWatchlist, updateWatchlist, autoGenerateWatchlist, getUserDataStatus, getUserPortfolio, getUserSocialPosts, getUserComputations, getUserVerification, getInstrumentMappings, searchPopularInvestors, requestPiAddition, getWatchlistTriggerCounts, checkPisInRankings, getPiProfile } = require('./helpers/data_helpers');
|
|
7
|
+
const { getPiAnalytics, getUserRecommendations, getWatchlist, updateWatchlist, autoGenerateWatchlist, getUserDataStatus, getUserPortfolio, getUserSocialPosts, getUserComputations, getUserVerification, getInstrumentMappings, searchPopularInvestors, requestPiAddition, getWatchlistTriggerCounts, checkPisInRankings, getPiProfile, checkIfUserIsPopularInvestor, trackProfileView, getSignedInUserPIPersonalizedMetrics } = require('./helpers/data_helpers');
|
|
8
8
|
const { initiateVerification, finalizeVerification } = require('./helpers/verification_helpers');
|
|
9
9
|
const { getUserWatchlists, getWatchlist: getWatchlistById, createWatchlist, updateWatchlist: updateWatchlistById, deleteWatchlist, copyWatchlist, getPublicWatchlists } = require('./helpers/watchlist_helpers');
|
|
10
10
|
const { subscribeToAlerts, updateSubscription, unsubscribeFromAlerts, getUserSubscriptions, subscribeToWatchlist } = require('./helpers/subscription_helpers');
|
|
@@ -26,6 +26,7 @@ module.exports = (dependencies, config) => {
|
|
|
26
26
|
// --- Data Serving ---
|
|
27
27
|
router.get('/pi/:cid/analytics', (req, res) => getPiAnalytics(req, res, dependencies, config));
|
|
28
28
|
router.get('/pi/:cid/profile', (req, res) => getPiProfile(req, res, dependencies, config));
|
|
29
|
+
router.post('/pi/:piCid/track-view', (req, res) => trackProfileView(req, res, dependencies, config));
|
|
29
30
|
|
|
30
31
|
// Signed-In User Personalized Routes
|
|
31
32
|
// Note: Router is mounted at /user, so these routes should be /me/* not /user/me/*
|
|
@@ -37,6 +38,8 @@ module.exports = (dependencies, config) => {
|
|
|
37
38
|
router.get('/me/computations', (req, res) => getUserComputations(req, res, dependencies, config));
|
|
38
39
|
router.get('/me/verification', (req, res) => getUserVerification(req, res, dependencies, config));
|
|
39
40
|
router.get('/me/instrument-mappings', (req, res) => getInstrumentMappings(req, res, dependencies, config));
|
|
41
|
+
router.get('/me/is-popular-investor', (req, res) => checkIfUserIsPopularInvestor(req, res, dependencies, config));
|
|
42
|
+
router.get('/me/pi-personalized-metrics', (req, res) => getSignedInUserPIPersonalizedMetrics(req, res, dependencies, config));
|
|
40
43
|
|
|
41
44
|
// --- Watchlist & Alerts ---
|
|
42
45
|
// Legacy single watchlist endpoints (for backward compatibility)
|