bulltrackers-module 1.0.594 → 1.0.596

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.
@@ -1517,7 +1517,7 @@ const finalizeVerification = async (db, pubsub, userId, username) => {
1517
1517
  isOptOut
1518
1518
  });
1519
1519
 
1520
- // 2. Create/Update User Doc in SignedInUsers
1520
+ // 2. Create/Update User Doc in SignedInUsers (legacy location - for backward compatibility)
1521
1521
  // Note: We use the realCID as the document ID, matching the generic-api logic
1522
1522
  await db.collection(signedInUsersCollection).doc(String(realCID)).set({
1523
1523
  username: profileData.username,
@@ -1529,7 +1529,30 @@ const finalizeVerification = async (db, pubsub, userId, username) => {
1529
1529
  lastLogin: FieldValue.serverTimestamp()
1530
1530
  }, { merge: true });
1531
1531
 
1532
- // 3. Trigger Downstream Systems via Task Engine
1532
+ // 3. Create/Update verification data in new format location: /SignedInUsers/{cid}/verification/data
1533
+ const verificationDataRef = db.collection('SignedInUsers').doc(String(realCID)).collection('verification').doc('data');
1534
+ const existingVerificationDoc = await verificationDataRef.get();
1535
+
1536
+ let emails = [];
1537
+ if (existingVerificationDoc.exists()) {
1538
+ const existingData = existingVerificationDoc.data();
1539
+ // Preserve existing email array if present
1540
+ emails = Array.isArray(existingData.email) ? existingData.email : (existingData.email ? [existingData.email] : []);
1541
+ }
1542
+ // Note: Email will be added by frontend completeAccountSetup when Firebase Auth email is available
1543
+
1544
+ await verificationDataRef.set({
1545
+ etoroUsername: profileData.username,
1546
+ etoroCID: realCID,
1547
+ email: emails, // Array - will be populated by frontend
1548
+ displayName: `${profileData.firstName || ''} ${profileData.lastName || ''}`.trim(),
1549
+ photoURL: profileData.avatars?.find(a => a.type === 'Original')?.url || null,
1550
+ verifiedAt: FieldValue.serverTimestamp(),
1551
+ accountSetupComplete: false, // Will be set to true by frontend completeAccountSetup
1552
+ createdAt: FieldValue.serverTimestamp(),
1553
+ }, { merge: true });
1554
+
1555
+ // 4. Trigger Downstream Systems via Task Engine
1533
1556
  if (pubsub) {
1534
1557
  try {
1535
1558
  // Replicating the 'unifiedTask' payload from generic-api
@@ -1875,10 +1898,82 @@ const checkDataStatus = async (db, userId) => {
1875
1898
  const snap = await db.collection('SignedInUsers').doc(userId).collection(type).doc('latest').get();
1876
1899
  status[type] = {
1877
1900
  exists: snap.exists,
1878
- updatedAt: snap.exists ? snap.updateTime.toDate() : null
1901
+ updatedAt: snap.exists ? snap.updateTime.toDate() : null,
1902
+ // Frontend-compatible fields
1903
+ available: snap.exists,
1904
+ lastUpdated: snap.exists ? snap.updateTime.toDate().toISOString() : null
1879
1905
  };
1880
1906
  }
1881
- return status;
1907
+
1908
+ // Find the latest computation date for profile metrics (7-day lookback)
1909
+ const lookbackDays = 7;
1910
+ const today = new Date();
1911
+ let computationDate = null;
1912
+ let fallbackWindowExhausted = false;
1913
+
1914
+ // Determine which computation to check based on user type
1915
+ let computationName = 'SignedInUserProfileMetrics';
1916
+ try {
1917
+ await fetchPopularInvestorMasterList(db, userId);
1918
+ computationName = 'SignedInUserPIPersonalizedMetrics';
1919
+ } catch (e) {
1920
+ // User is not a PI, use default
1921
+ }
1922
+
1923
+ // Check for computation results in the last 7 days
1924
+ for (let i = 0; i < lookbackDays; i++) {
1925
+ const checkDate = new Date(today);
1926
+ checkDate.setDate(checkDate.getDate() - i);
1927
+ const dateStr = checkDate.toISOString().split('T')[0];
1928
+
1929
+ try {
1930
+ const pageRef = db.collection('unified_insights')
1931
+ .doc(dateStr)
1932
+ .collection('results')
1933
+ .doc('popular-investor')
1934
+ .collection('computations')
1935
+ .doc(computationName)
1936
+ .collection('pages')
1937
+ .doc(String(userId));
1938
+
1939
+ const pageSnap = await pageRef.get();
1940
+ if (pageSnap.exists) {
1941
+ computationDate = dateStr;
1942
+ break;
1943
+ }
1944
+ } catch (error) {
1945
+ // Continue checking other dates
1946
+ console.error(`Error checking computation for ${dateStr}:`, error);
1947
+ }
1948
+ }
1949
+
1950
+ // If no computation found in 7 days, mark fallback window as exhausted
1951
+ if (!computationDate) {
1952
+ fallbackWindowExhausted = true;
1953
+ }
1954
+
1955
+ // Return frontend-compatible format
1956
+ return {
1957
+ portfolio: {
1958
+ ...status.portfolio,
1959
+ available: status.portfolio?.exists || false,
1960
+ lastUpdated: status.portfolio?.updatedAt ? status.portfolio.updatedAt.toISOString() : null
1961
+ },
1962
+ tradeHistory: {
1963
+ ...status.tradeHistory,
1964
+ available: status.tradeHistory?.exists || false,
1965
+ lastUpdated: status.tradeHistory?.updatedAt ? status.tradeHistory.updatedAt.toISOString() : null
1966
+ },
1967
+ posts: {
1968
+ ...status.posts,
1969
+ available: status.posts?.exists || false,
1970
+ lastUpdated: status.posts?.updatedAt ? status.posts.updatedAt.toISOString() : null
1971
+ },
1972
+ // Top-level fields for frontend
1973
+ portfolioAvailable: status.portfolio?.exists || false,
1974
+ computationDate: computationDate,
1975
+ fallbackWindowExhausted: fallbackWindowExhausted
1976
+ };
1882
1977
  };
1883
1978
 
1884
1979
  const sendTestAlert = async (db, userId, payload) => {
@@ -1,18 +1,52 @@
1
1
  /**
2
2
  * Middleware to resolve the effective user ID, handling Developer Impersonation.
3
3
  * Sets req.targetUserId, req.isImpersonating, and req.actualUserId.
4
+ *
5
+ * Public routes (that don't require authentication):
6
+ * - /watchlists/public
7
+ * - /popular-investors/trending
8
+ * - /popular-investors/categories
9
+ * - /popular-investors/master-list
10
+ * - /popular-investors/search
4
11
  */
5
12
  const { isDeveloper } = require('../helpers/data-fetchers/firestore.js'); // Using your provided helper
6
13
 
14
+ // List of public routes that don't require userCid
15
+ const PUBLIC_ROUTES = [
16
+ '/watchlists/public',
17
+ '/popular-investors/trending',
18
+ '/popular-investors/categories',
19
+ '/popular-investors/master-list',
20
+ '/popular-investors/search'
21
+ ];
22
+
23
+ const isPublicRoute = (path, originalUrl) => {
24
+ // Check both the path and originalUrl to handle Express routing
25
+ const fullPath = originalUrl || path;
26
+ return PUBLIC_ROUTES.some(route => fullPath.includes(route));
27
+ };
28
+
7
29
  const resolveUserIdentity = async (req, res, next) => {
8
30
  try {
31
+ // Check if this is a public route (check both path and originalUrl for Express routing)
32
+ const isPublic = isPublicRoute(req.path, req.originalUrl);
33
+
9
34
  // 1. Identify the actual authenticated user (from Auth middleware or params)
10
35
  const actualUserId = req.query.userCid || req.body.userCid || req.headers['x-user-cid'];
11
36
 
12
- if (!actualUserId) {
37
+ // For public routes, userCid is optional
38
+ if (!actualUserId && !isPublic) {
13
39
  return res.status(400).json({ error: "Missing user identification (userCid)" });
14
40
  }
15
41
 
42
+ // If no user ID provided and it's a public route, skip identity resolution
43
+ if (!actualUserId && isPublic) {
44
+ req.actualUserId = null;
45
+ req.targetUserId = null;
46
+ req.isImpersonating = false;
47
+ return next();
48
+ }
49
+
16
50
  // 2. Check for Impersonation Request (Headers or Query)
17
51
  const impersonateId = req.headers['x-impersonate-cid'] || req.query.impersonateCid;
18
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.594",
3
+ "version": "1.0.596",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [