bulltrackers-module 1.0.591 → 1.0.592

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +6 -6
  2. package/functions/alert-system/index.js +1 -1
  3. package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
  4. package/functions/api-v2/helpers/task_engine_helper.js +51 -0
  5. package/functions/api-v2/index.js +36 -0
  6. package/functions/api-v2/middleware/identity_middleware.js +48 -0
  7. package/functions/api-v2/package.json +6 -0
  8. package/functions/api-v2/routes/alerts.js +168 -0
  9. package/functions/api-v2/routes/index.js +35 -0
  10. package/functions/api-v2/routes/notifications.js +38 -0
  11. package/functions/api-v2/routes/popular_investors.js +204 -0
  12. package/functions/api-v2/routes/profile.js +212 -0
  13. package/functions/api-v2/routes/reviews.js +72 -0
  14. package/functions/api-v2/routes/settings.js +71 -0
  15. package/functions/api-v2/routes/sync.js +132 -0
  16. package/functions/api-v2/routes/verification.js +47 -0
  17. package/functions/api-v2/routes/watchlists.js +148 -0
  18. package/functions/computation-system/helpers/computation_worker.js +1 -1
  19. package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
  20. package/index.js +6 -2
  21. package/package.json +2 -1
  22. package/functions/generic-api/admin-api/index.js +0 -895
  23. package/functions/generic-api/helpers/api_helpers.js +0 -457
  24. package/functions/generic-api/index.js +0 -204
  25. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
  26. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
  27. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
  28. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
  29. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
  30. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
  31. package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
  32. package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
  33. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
  34. package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
  35. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
  36. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
  37. package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
  38. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
  39. package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
  40. package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
  41. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
  42. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
  43. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
  44. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
  45. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
  46. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
  47. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
  48. package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
  49. package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
  50. package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
  51. package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
  52. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
  53. package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
  54. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
  55. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
  56. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
  57. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
  58. package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
  59. package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
  60. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
  61. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
  62. package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
  63. package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
  64. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
  65. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
  66. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
  67. package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
  68. package/functions/generic-api/user-api/index.js +0 -109
@@ -1,323 +0,0 @@
1
- /**
2
- * @fileoverview Logic for User Verification (OTP Bio Check) and On-Demand Data Triggers.
3
- */
4
- const { IntelligentProxyManager } = require('../../../../core/utils/intelligent_proxy_manager');
5
- const { IntelligentHeaderManager } = require('../../../../core/utils/intelligent_header_manager');
6
- const { PubSubUtils } = require('../../../../core/utils/pubsub_utils');
7
- const { FieldValue } = require('@google-cloud/firestore');
8
-
9
- /**
10
- * 1. Initiate Verification
11
- * Generates an OTP and returns it to the user to place in their bio.
12
- */
13
- async function initiateVerification(req, res, dependencies, config) {
14
- const { db, logger } = dependencies;
15
- const { username } = req.body;
16
-
17
- // Safety check: ensure config exists
18
- if (!config) {
19
- logger?.log('ERROR', '[Verification] Config is undefined or null in initiateVerification');
20
- return res.status(500).json({ error: "Configuration not initialized." });
21
- }
22
-
23
- const { verificationsCollection } = config;
24
-
25
- // Safety check: ensure required config values exist
26
- if (!verificationsCollection) {
27
- logger?.log('ERROR', '[Verification] verificationsCollection is not defined in config');
28
- return res.status(500).json({ error: "Configuration error: verificationsCollection not initialized." });
29
- }
30
-
31
- if (!username || typeof username !== 'string') {
32
- return res.status(400).json({ error: "Invalid username." });
33
- }
34
-
35
- try {
36
- // Generate a simple 6-char alphanumeric OTP
37
- const otp = Math.random().toString(36).substring(2, 8).toUpperCase();
38
-
39
- // Store pending request
40
- // Key by username to prevent multiple active requests per user
41
- await db.collection(verificationsCollection).doc(username.toLowerCase()).set({
42
- username,
43
- otp,
44
- status: 'PENDING',
45
- createdAt: FieldValue.serverTimestamp(),
46
- expiresAt: new Date(Date.now() + 3600000) // 1 hour expiry
47
- });
48
-
49
- return res.status(200).json({
50
- success: true,
51
- message: "Please place this code in your eToro Bio or Short Bio.",
52
- otp
53
- });
54
-
55
- } catch (error) {
56
- return res.status(500).json({ error: error.message });
57
- }
58
- }
59
-
60
- /**
61
- * 2. Finalize Verification
62
- * Checks eToro API for the OTP, claims the user, and triggers on-demand fetching.
63
- */
64
- async function finalizeVerification(req, res, dependencies, config) {
65
- const { db, logger } = dependencies;
66
- const { username } = req.body;
67
-
68
- // Safety check: ensure config exists and is an object
69
- if (!config || typeof config !== 'object') {
70
- logger.log('ERROR', '[Verification] Config is undefined, null, or not an object', { configType: typeof config, configValue: config });
71
- return res.status(500).json({ error: "Configuration not initialized." });
72
- }
73
-
74
- // Safe destructuring with defaults to prevent ReferenceErrors
75
- const {
76
- verificationsCollection = null,
77
- signedInUsersCollection = null,
78
- proxyConfig = null,
79
- headerConfig = null,
80
- pubsubTopicUserFetch = null,
81
- pubsubTopicUserFetchOnDemand = null,
82
- pubsubTopicSocialFetch = null
83
- } = config || {};
84
-
85
- // Safety check: ensure required config values exist
86
- if (!signedInUsersCollection) {
87
- logger.log('ERROR', '[Verification] signedInUsersCollection is not defined in config', {
88
- configKeys: Object.keys(config),
89
- signedInUsersCollection: config.signedInUsersCollection
90
- });
91
- return res.status(500).json({ error: "Configuration error: signedInUsersCollection not initialized." });
92
- }
93
-
94
- if (!verificationsCollection) {
95
- logger.log('ERROR', '[Verification] verificationsCollection is not defined in config', {
96
- configKeys: Object.keys(config),
97
- verificationsCollection: config.verificationsCollection
98
- });
99
- return res.status(500).json({ error: "Configuration error: verificationsCollection not initialized." });
100
- }
101
-
102
- // Use on-demand topic for user signup (API-triggered)
103
- const taskEngineTopic = pubsubTopicUserFetchOnDemand || pubsubTopicUserFetch || 'etoro-user-fetch-topic-ondemand';
104
-
105
- if (!username) return res.status(400).json({ error: "Missing username." });
106
-
107
- try {
108
- const docRef = db.collection(verificationsCollection).doc(username.toLowerCase());
109
- const doc = await docRef.get();
110
-
111
- if (!doc.exists) {
112
- return res.status(404).json({ error: "No pending verification found. Please initiate first." });
113
- }
114
-
115
- const verificationData = doc.data();
116
- if (verificationData.status === 'VERIFIED') {
117
- return res.status(200).json({ success: true, message: "User already verified." });
118
- }
119
-
120
- const otp = verificationData.otp;
121
-
122
- // --- A. Fetch eToro Profile ---
123
- const proxyManager = new IntelligentProxyManager(db, logger, proxyConfig);
124
- const headerManager = new IntelligentHeaderManager(db, logger, headerConfig);
125
-
126
- logger.log('INFO', `[Verification] Fetching profile for ${username} via Proxy/Header managers...`);
127
-
128
- // Select Header
129
- const { header } = await headerManager.selectHeader();
130
- const requestHeaders = {
131
- 'Accept': 'application/json',
132
- 'Referer': 'https://www.etoro.com/',
133
- ...header
134
- };
135
-
136
- const targetUrl = `https://www.etoro.com/api/logininfo/v1.1/users/${username}`;
137
-
138
- let profileData = null;
139
- try {
140
- const response = await proxyManager.fetch(targetUrl, {
141
- method: 'GET',
142
- headers: requestHeaders
143
- });
144
-
145
- if (response.ok) {
146
- profileData = await response.json();
147
- } else {
148
- // [FIX] Fallback to direct fetch with proper error handling
149
- logger.log('WARN', `[Verification] Proxy failed (${response.status}). Trying direct fetch...`);
150
- try {
151
- const directFetch = typeof fetch !== 'undefined' ? fetch : require('node-fetch');
152
- const directRes = await directFetch(targetUrl, { headers: requestHeaders });
153
- if (directRes.ok) {
154
- profileData = await directRes.json();
155
- } else {
156
- throw new Error(`eToro API status: ${directRes.status}`);
157
- }
158
- } catch (directErr) {
159
- logger.log('ERROR', `[Verification] Direct fetch also failed for ${username}`, directErr);
160
- throw new Error(`Failed to fetch eToro profile. Proxy: ${response.status}, Direct: ${directErr.message}`);
161
- }
162
- }
163
- } catch (fetchErr) {
164
- logger.log('ERROR', `[Verification] Fetch failed for ${username}`, fetchErr);
165
- return res.status(502).json({ error: "Failed to connect to eToro. Try again later." });
166
- }
167
-
168
- if (!profileData || !profileData.realCID) {
169
- return res.status(404).json({ error: "eToro user not found or invalid response." });
170
- }
171
-
172
- // --- B. Validate OTP ---
173
- const bio = profileData.userBio?.aboutMe || profileData.aboutMe || "";
174
- const bioShort = profileData.userBio?.aboutMeShort || profileData.aboutMeShort || "";
175
-
176
- if (!bio.includes(otp) && !bioShort.includes(otp)) {
177
- return res.status(400).json({
178
- success: false,
179
- error: "OTP not found in Bio.",
180
- debug: "Ensure the code is saved in your 'About Me' section."
181
- });
182
- }
183
-
184
- // --- C. Success: Store & Trigger ---
185
- const realCID = profileData.realCID;
186
- const isOptOut = profileData.optOut === true; // Private profile
187
-
188
- // 1. Mark Verified
189
- await docRef.update({
190
- status: 'VERIFIED',
191
- verifiedAt: FieldValue.serverTimestamp(),
192
- cid: realCID,
193
- isOptOut
194
- });
195
-
196
- // 2. Create/Update User Doc
197
- await db.collection(signedInUsersCollection).doc(String(realCID)).set({
198
- username: profileData.username,
199
- cid: realCID,
200
- fullName: `${profileData.firstName || ''} ${profileData.lastName || ''}`.trim(),
201
- avatar: profileData.avatars?.find(a => a.type === 'Original')?.url || null,
202
- isOptOut,
203
- verifiedAt: FieldValue.serverTimestamp(),
204
- lastLogin: FieldValue.serverTimestamp()
205
- }, { merge: true });
206
-
207
- // 3. Trigger Downstream Systems via Pub/Sub
208
- // Send unified request to task engine that handles both portfolio/history AND social data
209
- const pubsubUtils = new PubSubUtils(dependencies);
210
-
211
- // Generate a requestId for tracking and to ensure finalizeOnDemandRequest runs
212
- // This is critical - without requestId, the root data indexer and computations won't be triggered
213
- const requestId = `signup-${realCID}-${Date.now()}`;
214
-
215
- // Create request tracking document (similar to user sync requests)
216
- try {
217
- const requestRef = db.collection('user_sync_requests')
218
- .doc(String(realCID))
219
- .collection('requests')
220
- .doc(requestId);
221
-
222
- await requestRef.set({
223
- targetUserCid: realCID,
224
- username: profileData.username,
225
- status: 'pending',
226
- source: 'user_signup',
227
- requestedAt: FieldValue.serverTimestamp(),
228
- createdAt: FieldValue.serverTimestamp(),
229
- metadata: {
230
- isNewUser: true,
231
- verificationSource: 'otp_verification'
232
- }
233
- });
234
-
235
- logger.log('INFO', `[Verification] Created request tracking document for ${username} (${realCID}): ${requestId}`);
236
- } catch (reqError) {
237
- logger.log('WARN', `[Verification] Failed to create request tracking document: ${reqError.message}`);
238
- // Continue anyway - the requestId is still set
239
- }
240
-
241
- // Create a unified on-demand request that includes both portfolio and social
242
- // The task engine will process both, update root data, then trigger computations
243
- const unifiedTask = {
244
- type: 'ON_DEMAND_USER_UPDATE', // Matches Task Engine handler
245
- cid: realCID, // Top-level CID for handler
246
- username: profileData.username, // Top-level username for handler
247
- requestId: requestId, // CRITICAL: Required for finalizeOnDemandRequest to run
248
- source: 'user_signup', // Mark as signup to ensure computations are triggered
249
- data: {
250
- cid: realCID,
251
- username: profileData.username,
252
- includeSocial: true, // Flag to include social data fetch
253
- since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString() // Last 7 days for social
254
- },
255
- metadata: {
256
- onDemand: true,
257
- targetCid: realCID, // Optimization: only process this user
258
- isNewUser: true, // Flag to add to daily update queue
259
- requestedAt: new Date().toISOString(),
260
- userType: 'SIGNED_IN_USER' // Explicitly set userType for computation selection
261
- }
262
- };
263
-
264
- // Only trigger if user is public (has portfolio data)
265
- if (!isOptOut) {
266
- await pubsubUtils.publish(taskEngineTopic, unifiedTask);
267
- logger.log('INFO', `[Verification] Triggered unified data fetch (portfolio + social) for ${username} (${realCID}) via on-demand topic with requestId: ${requestId}`);
268
- } else {
269
- // For private users, still fetch social data but no portfolio
270
- const socialOnlyTask = {
271
- type: 'ON_DEMAND_USER_UPDATE',
272
- cid: realCID,
273
- username: profileData.username,
274
- requestId: requestId, // CRITICAL: Required for finalizeOnDemandRequest to run
275
- source: 'user_signup',
276
- data: {
277
- cid: realCID,
278
- username: profileData.username,
279
- includeSocial: true,
280
- portfolioOnly: false, // Skip portfolio for private users
281
- since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString()
282
- },
283
- metadata: {
284
- onDemand: true,
285
- targetCid: realCID,
286
- isNewUser: true,
287
- requestedAt: new Date().toISOString(),
288
- userType: 'SIGNED_IN_USER' // Explicitly set userType for computation selection
289
- }
290
- };
291
- await pubsubUtils.publish(taskEngineTopic, socialOnlyTask);
292
- logger.log('INFO', `[Verification] Triggered social-only fetch for private user ${username} (${realCID}) via on-demand topic with requestId: ${requestId}`);
293
- }
294
-
295
- return res.status(200).json({
296
- success: true,
297
- message: "Account verified successfully. Data ingestion started.",
298
- cid: realCID
299
- });
300
-
301
- } catch (error) {
302
- // Enhanced error logging to capture full error details
303
- const errorDetails = {
304
- message: error?.message || 'Unknown error',
305
- stack: error?.stack || 'No stack trace',
306
- name: error?.name || 'Error',
307
- configExists: !!config,
308
- configKeys: config ? Object.keys(config) : [],
309
- signedInUsersCollection: config?.signedInUsersCollection || 'NOT SET',
310
- verificationsCollection: config?.verificationsCollection || 'NOT SET'
311
- };
312
-
313
- logger.log('ERROR', `[Verification] System error for ${username}`, errorDetails);
314
- logger.log('ERROR', `[Verification] Full error object:`, error);
315
-
316
- return res.status(500).json({
317
- error: error?.message || 'An unexpected error occurred during verification.',
318
- details: process.env.NODE_ENV === 'development' ? errorDetails : undefined
319
- });
320
- }
321
- }
322
-
323
- module.exports = { initiateVerification, finalizeVerification };
@@ -1,96 +0,0 @@
1
- /**
2
- * @fileoverview Watchlist Analytics Helpers
3
- * Handles watchlist analytics and trigger counts
4
- */
5
-
6
- const { Timestamp } = require('@google-cloud/firestore');
7
- const { readWithMigration } = require('../core/path_resolution_helpers');
8
-
9
- /**
10
- * GET /user/me/watchlists/:id/trigger-counts
11
- * Get alert trigger counts for PIs in a watchlist (last 7 days)
12
- */
13
- async function getWatchlistTriggerCounts(req, res, dependencies, config) {
14
- const { db, logger } = dependencies;
15
- const { userCid } = req.query;
16
- const { id } = req.params;
17
-
18
- if (!userCid || !id) {
19
- return res.status(400).json({ error: "Missing userCid or watchlist id" });
20
- }
21
-
22
- try {
23
- // Read watchlist from new path with migration
24
- const watchlistResult = await readWithMigration(
25
- db,
26
- 'signedInUsers',
27
- 'watchlists',
28
- { cid: userCid },
29
- {
30
- isCollection: false,
31
- dataType: 'watchlists',
32
- config,
33
- logger,
34
- documentId: id,
35
- collectionRegistry: dependencies.collectionRegistry
36
- }
37
- );
38
-
39
- let items = [];
40
- if (watchlistResult && watchlistResult.data) {
41
- items = watchlistResult.data.items || [];
42
- } else {
43
- // Fallback to legacy path
44
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
45
- const watchlistRef = db.collection(watchlistsCollection)
46
- .doc(String(userCid))
47
- .collection('lists')
48
- .doc(id);
49
-
50
- const watchlistDoc = await watchlistRef.get();
51
-
52
- if (!watchlistDoc.exists) {
53
- return res.status(404).json({ error: "Watchlist not found" });
54
- }
55
-
56
- const watchlistData = watchlistDoc.data();
57
- items = watchlistData.items || [];
58
- }
59
-
60
- // Calculate date 7 days ago
61
- const sevenDaysAgo = new Date();
62
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
63
- const cutoffTimestamp = Timestamp.fromDate(sevenDaysAgo);
64
-
65
- const triggerCounts = {};
66
- const alertTriggersCollection = config.alertTriggersCollection || 'alert_triggers';
67
-
68
- // For each PI in the watchlist, count triggers in last 7 days
69
- for (const item of items) {
70
- const piCid = item.cid;
71
- const triggersRef = db.collection(alertTriggersCollection)
72
- .doc(String(userCid))
73
- .collection('triggers')
74
- .where('piCid', '==', piCid)
75
- .where('triggeredAt', '>', cutoffTimestamp);
76
-
77
- const triggersSnapshot = await triggersRef.get();
78
- triggerCounts[piCid] = triggersSnapshot.size;
79
- }
80
-
81
- return res.status(200).json({
82
- triggerCounts,
83
- watchlistId: id,
84
- period: '7days'
85
- });
86
-
87
- } catch (error) {
88
- logger.log('ERROR', `[getWatchlistTriggerCounts] Error fetching trigger counts`, error);
89
- return res.status(500).json({ error: error.message });
90
- }
91
- }
92
-
93
- module.exports = {
94
- getWatchlistTriggerCounts
95
- };
96
-
@@ -1,141 +0,0 @@
1
- /**
2
- * @fileoverview Legacy Watchlist Data Helpers
3
- * Handles legacy single watchlist endpoints with migration support
4
- */
5
-
6
- const { FieldValue } = require('@google-cloud/firestore');
7
- const { readWithMigration, writeWithMigration } = require('../core/path_resolution_helpers');
8
- const { getEffectiveCid } = require('../dev/dev_helpers');
9
-
10
- /**
11
- * GET /user/me/watchlist (Legacy)
12
- * Fetches the user's legacy watchlist
13
- * Migrates to new path automatically
14
- */
15
- async function getWatchlist(req, res, dependencies, config) {
16
- const { db, logger } = dependencies;
17
- const { userCid } = req.query;
18
-
19
- if (!userCid) {
20
- return res.status(400).json({ error: "Missing userCid" });
21
- }
22
-
23
- try {
24
- const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
25
-
26
- // Try new path with migration
27
- const result = await readWithMigration(
28
- db,
29
- 'signedInUsers',
30
- 'watchlists',
31
- { cid: effectiveCid },
32
- {
33
- isCollection: true,
34
- dataType: 'watchlists',
35
- config,
36
- logger,
37
- collectionRegistry: dependencies.collectionRegistry
38
- }
39
- );
40
-
41
- if (result && result.snapshot) {
42
- // Convert snapshot to legacy format for backward compatibility
43
- const watchlistData = {};
44
- result.snapshot.forEach(doc => {
45
- watchlistData[doc.id] = doc.data();
46
- });
47
-
48
- return res.status(200).json({
49
- watchlist: watchlistData,
50
- effectiveCid: effectiveCid,
51
- actualCid: Number(userCid),
52
- migrated: result.source === 'legacy'
53
- });
54
- }
55
-
56
- // Fallback to legacy collection
57
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
58
- const watchlistRef = db.collection(watchlistsCollection).doc(String(effectiveCid));
59
- const watchlistDoc = await watchlistRef.get();
60
-
61
- if (!watchlistDoc.exists) {
62
- return res.status(200).json({ watchlist: {} });
63
- }
64
-
65
- const watchlistData = watchlistDoc.data();
66
- return res.status(200).json({
67
- watchlist: watchlistData,
68
- effectiveCid: effectiveCid,
69
- actualCid: Number(userCid)
70
- });
71
-
72
- } catch (error) {
73
- logger.log('ERROR', `[getWatchlist] Error fetching watchlist for ${userCid}`, error);
74
- return res.status(500).json({ error: error.message });
75
- }
76
- }
77
-
78
- /**
79
- * POST /watchlist (Legacy)
80
- * Adds/Updates a watchlist item
81
- * Writes to both new and legacy paths during migration
82
- */
83
- async function updateWatchlist(req, res, dependencies, config) {
84
- const { db, logger } = dependencies;
85
- const { userCid, type, target, alertConfig } = req.body;
86
-
87
- if (!userCid || !type || !target) {
88
- return res.status(400).json({ error: "Invalid payload" });
89
- }
90
-
91
- try {
92
- const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
93
-
94
- // Generate entry ID
95
- const entryId = type === 'static'
96
- ? `static_${target}`
97
- : `dyn_${Buffer.from(JSON.stringify(target)).toString('base64').slice(0,10)}`;
98
-
99
- const watchlistEntry = {
100
- type,
101
- target,
102
- alertConfig: alertConfig || {},
103
- updatedAt: FieldValue.serverTimestamp()
104
- };
105
-
106
- // Write to new path with dual write
107
- await writeWithMigration(
108
- db,
109
- 'signedInUsers',
110
- 'watchlists',
111
- { cid: effectiveCid },
112
- watchlistEntry,
113
- {
114
- isCollection: true,
115
- merge: true,
116
- dataType: 'watchlists',
117
- config,
118
- documentId: entryId,
119
- dualWrite: true,
120
- collectionRegistry: dependencies.collectionRegistry
121
- }
122
- );
123
-
124
- return res.status(200).json({
125
- success: true,
126
- id: entryId,
127
- effectiveCid: effectiveCid,
128
- actualCid: Number(userCid)
129
- });
130
-
131
- } catch (error) {
132
- logger.log('ERROR', `[updateWatchlist] Error updating watchlist for ${userCid}`, error);
133
- return res.status(500).json({ error: error.message });
134
- }
135
- }
136
-
137
- module.exports = {
138
- getWatchlist,
139
- updateWatchlist
140
- };
141
-