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.
Files changed (49) hide show
  1. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
  2. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
  3. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
  4. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
  5. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
  6. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
  7. package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
  8. package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
  9. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
  10. package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
  11. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
  12. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
  13. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  14. package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
  15. package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
  16. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  17. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
  18. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
  19. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
  20. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
  21. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  22. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
  23. package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
  24. package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
  25. package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
  26. package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
  27. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
  28. package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
  29. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
  30. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
  31. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
  32. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
  33. package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
  34. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
  35. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
  36. package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
  37. package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
  38. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
  39. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
  40. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
  41. package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
  42. package/functions/generic-api/user-api/index.js +9 -9
  43. package/functions/task-engine/handler_creator.js +7 -6
  44. package/package.json +1 -1
  45. package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
  46. package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
  47. package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
  48. package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
  49. /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
+