bulltrackers-module 1.0.629 → 1.0.631

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 (42) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +69 -77
  2. package/functions/alert-system/index.js +19 -29
  3. package/functions/api-v2/helpers/notification_helpers.js +187 -0
  4. package/functions/computation-system/helpers/computation_worker.js +1 -1
  5. package/functions/task-engine/helpers/popular_investor_helpers.js +11 -7
  6. package/index.js +0 -5
  7. package/package.json +1 -2
  8. package/functions/old-generic-api/admin-api/index.js +0 -895
  9. package/functions/old-generic-api/helpers/api_helpers.js +0 -457
  10. package/functions/old-generic-api/index.js +0 -204
  11. package/functions/old-generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
  12. package/functions/old-generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
  13. package/functions/old-generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
  14. package/functions/old-generic-api/user-api/helpers/collection_helpers.js +0 -193
  15. package/functions/old-generic-api/user-api/helpers/core/compression_helpers.js +0 -68
  16. package/functions/old-generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
  17. package/functions/old-generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
  18. package/functions/old-generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
  19. package/functions/old-generic-api/user-api/helpers/data/computation_helpers.js +0 -503
  20. package/functions/old-generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
  21. package/functions/old-generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
  22. package/functions/old-generic-api/user-api/helpers/data/social_helpers.js +0 -174
  23. package/functions/old-generic-api/user-api/helpers/data_helpers.js +0 -87
  24. package/functions/old-generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
  25. package/functions/old-generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
  26. package/functions/old-generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
  27. package/functions/old-generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
  28. package/functions/old-generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
  29. package/functions/old-generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
  30. package/functions/old-generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
  31. package/functions/old-generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
  32. package/functions/old-generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
  33. package/functions/old-generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
  34. package/functions/old-generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
  35. package/functions/old-generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
  36. package/functions/old-generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
  37. package/functions/old-generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
  38. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
  39. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
  40. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
  41. package/functions/old-generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
  42. package/functions/old-generic-api/user-api/index.js +0 -109
@@ -1,829 +0,0 @@
1
- /**
2
- * @fileoverview Watchlist Management Helpers
3
- * Handles CRUD operations for user watchlists (static and dynamic)
4
- */
5
-
6
- const { FieldValue } = require('@google-cloud/firestore');
7
- const crypto = require('crypto');
8
-
9
- /**
10
- * Generate unique watchlist ID
11
- */
12
- function generateWatchlistId() {
13
- return `watchlist_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
14
- }
15
-
16
- /**
17
- * GET /user/me/watchlists
18
- * List all watchlists for a user
19
- */
20
- async function getUserWatchlists(req, res, dependencies, config) {
21
- const { db, logger } = dependencies;
22
- const { userCid } = req.query;
23
-
24
- if (!userCid) {
25
- return res.status(400).json({ error: "Missing userCid" });
26
- }
27
-
28
- try {
29
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
30
- const userWatchlistsRef = db.collection(watchlistsCollection)
31
- .doc(String(userCid))
32
- .collection('lists');
33
-
34
- const snapshot = await userWatchlistsRef.get();
35
-
36
- const watchlists = [];
37
- snapshot.forEach(doc => {
38
- watchlists.push({
39
- id: doc.id,
40
- ...doc.data()
41
- });
42
- });
43
-
44
- // Sort by creation date (newest first)
45
- watchlists.sort((a, b) => {
46
- const aTime = a.createdAt?.seconds || 0;
47
- const bTime = b.createdAt?.seconds || 0;
48
- return bTime - aTime;
49
- });
50
-
51
- return res.status(200).json({
52
- watchlists,
53
- count: watchlists.length
54
- });
55
-
56
- } catch (error) {
57
- logger.log('ERROR', `[getUserWatchlists] Error fetching watchlists for ${userCid}`, error);
58
- return res.status(500).json({ error: error.message });
59
- }
60
- }
61
-
62
- /**
63
- * GET /user/me/watchlists/:id
64
- * Get a specific watchlist
65
- */
66
- async function getWatchlist(req, res, dependencies, config) {
67
- const { db, logger } = dependencies;
68
- const { userCid } = req.query;
69
- const { id } = req.params;
70
-
71
- if (!userCid || !id) {
72
- return res.status(400).json({ error: "Missing userCid or watchlist id" });
73
- }
74
-
75
- try {
76
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
77
- const watchlistRef = db.collection(watchlistsCollection)
78
- .doc(String(userCid))
79
- .collection('lists')
80
- .doc(id);
81
-
82
- const watchlistDoc = await watchlistRef.get();
83
-
84
- if (!watchlistDoc.exists) {
85
- return res.status(404).json({ error: "Watchlist not found" });
86
- }
87
-
88
- return res.status(200).json({
89
- id: watchlistDoc.id,
90
- ...watchlistDoc.data()
91
- });
92
-
93
- } catch (error) {
94
- logger.log('ERROR', `[getWatchlist] Error fetching watchlist ${id} for ${userCid}`, error);
95
- return res.status(500).json({ error: error.message });
96
- }
97
- }
98
-
99
- /**
100
- * POST /user/me/watchlists
101
- * Create a new watchlist
102
- */
103
- async function createWatchlist(req, res, dependencies, config) {
104
- const { db, logger } = dependencies;
105
- const { userCid, name, type, visibility = 'private', items, dynamicConfig } = req.body;
106
-
107
- if (!userCid || !name || !type) {
108
- return res.status(400).json({ error: "Missing required fields: userCid, name, type" });
109
- }
110
-
111
- if (type !== 'static' && type !== 'dynamic') {
112
- return res.status(400).json({ error: "Type must be 'static' or 'dynamic'" });
113
- }
114
-
115
- if (visibility !== 'public' && visibility !== 'private') {
116
- return res.status(400).json({ error: "Visibility must be 'public' or 'private'" });
117
- }
118
-
119
- try {
120
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
121
- const watchlistId = generateWatchlistId();
122
-
123
- const watchlistData = {
124
- id: watchlistId,
125
- name: name.trim(),
126
- type,
127
- visibility,
128
- createdBy: Number(userCid),
129
- createdAt: FieldValue.serverTimestamp(),
130
- updatedAt: FieldValue.serverTimestamp(),
131
- isAutoGenerated: false,
132
- copyCount: 0
133
- };
134
-
135
- if (type === 'static') {
136
- watchlistData.items = items || [];
137
- } else if (type === 'dynamic') {
138
- if (!dynamicConfig || !dynamicConfig.computationName) {
139
- return res.status(400).json({ error: "Dynamic watchlists require dynamicConfig with computationName" });
140
- }
141
- watchlistData.dynamicConfig = dynamicConfig;
142
- }
143
-
144
- // Write to new SignedInUsers path with dual-write to legacy path
145
- const { writeWithMigration } = require('../core/path_resolution_helpers');
146
- await writeWithMigration(
147
- db,
148
- 'signedInUsers',
149
- 'watchlists',
150
- { cid: userCid },
151
- watchlistData,
152
- {
153
- isCollection: true,
154
- merge: false,
155
- dataType: 'watchlists',
156
- config,
157
- collectionRegistry: dependencies.collectionRegistry,
158
- documentId: watchlistId,
159
- dualWrite: true
160
- }
161
- );
162
-
163
- // Update global rootdata collection for computation system if static watchlist has items
164
- if (type === 'static' && items && items.length > 0) {
165
- const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
166
- const today = new Date().toISOString().split('T')[0];
167
- const isPublic = visibility === 'public';
168
-
169
- for (const item of items) {
170
- const piCid = String(item.cid);
171
- // Update global WatchlistMembershipData/{date} document
172
- await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
173
- // Update PI-centric PopularInvestors/{piCid}/watchlistData
174
- await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
175
- }
176
- }
177
-
178
- // Watchlists are always created as private - no public version on creation
179
-
180
- logger.log('SUCCESS', `[createWatchlist] Created ${type} watchlist "${name}" for user ${userCid}`);
181
-
182
- return res.status(201).json({
183
- success: true,
184
- watchlist: {
185
- id: watchlistId,
186
- ...watchlistData
187
- }
188
- });
189
-
190
- } catch (error) {
191
- logger.log('ERROR', `[createWatchlist] Error creating watchlist for ${userCid}`, error);
192
- return res.status(500).json({ error: error.message });
193
- }
194
- }
195
-
196
- /**
197
- * PUT /user/me/watchlists/:id
198
- * Update a watchlist
199
- */
200
- async function updateWatchlist(req, res, dependencies, config) {
201
- const { db, logger } = dependencies;
202
- const { userCid } = req.query;
203
- const { id } = req.params;
204
- const { name, visibility, items, dynamicConfig } = req.body;
205
-
206
- if (!userCid || !id) {
207
- return res.status(400).json({ error: "Missing userCid or watchlist id" });
208
- }
209
-
210
- try {
211
- // Read from new path with migration fallback
212
- const { readWithMigration } = require('../core/path_resolution_helpers');
213
- const readResult = await readWithMigration(
214
- db,
215
- 'signedInUsers',
216
- 'watchlists',
217
- { cid: userCid },
218
- {
219
- isCollection: true,
220
- dataType: 'watchlists',
221
- config,
222
- logger,
223
- collectionRegistry: dependencies.collectionRegistry,
224
- documentId: id
225
- }
226
- );
227
-
228
- if (!readResult) {
229
- return res.status(404).json({ error: "Watchlist not found" });
230
- }
231
-
232
- // Handle both document read (with data) and collection read (with snapshot)
233
- let existingData;
234
- if (readResult.data) {
235
- existingData = readResult.data;
236
- } else if (readResult.snapshot && !readResult.snapshot.empty) {
237
- // If we got a snapshot, find the specific document
238
- const doc = readResult.snapshot.docs.find(d => d.id === id);
239
- if (!doc) {
240
- return res.status(404).json({ error: "Watchlist not found" });
241
- }
242
- existingData = doc.data();
243
- } else {
244
- return res.status(404).json({ error: "Watchlist not found" });
245
- }
246
-
247
- // Verify ownership
248
- if (existingData.createdBy !== Number(userCid)) {
249
- return res.status(403).json({ error: "You can only modify your own watchlists" });
250
- }
251
-
252
- // Check if this is a copied watchlist
253
- const isCopiedWatchlist = existingData.copiedFrom && existingData.copiedFromCreator;
254
- const wasModified = existingData.hasBeenModified || false;
255
-
256
- // Determine if meaningful changes are being made (not just name)
257
- let hasMeaningfulChanges = wasModified;
258
- if (items !== undefined && existingData.type === 'static') {
259
- // Check if items actually changed
260
- const itemsChanged = JSON.stringify(items) !== JSON.stringify(existingData.items || []);
261
- hasMeaningfulChanges = hasMeaningfulChanges || itemsChanged;
262
- }
263
- if (dynamicConfig !== undefined && existingData.type === 'dynamic') {
264
- // Check if dynamicConfig actually changed
265
- const configChanged = JSON.stringify(dynamicConfig) !== JSON.stringify(existingData.dynamicConfig || {});
266
- hasMeaningfulChanges = hasMeaningfulChanges || configChanged;
267
- }
268
-
269
- // If trying to make a copied watchlist public, check if meaningful changes were made
270
- if (visibility === 'public' && isCopiedWatchlist && !hasMeaningfulChanges) {
271
- return res.status(400).json({
272
- error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
273
- });
274
- }
275
-
276
- const updates = {
277
- updatedAt: FieldValue.serverTimestamp()
278
- };
279
-
280
- if (name !== undefined) {
281
- updates.name = name.trim();
282
- }
283
-
284
- // Track if watchlist has been modified (for copied watchlists)
285
- if (isCopiedWatchlist && hasMeaningfulChanges) {
286
- updates.hasBeenModified = true;
287
- }
288
-
289
- // Visibility changes are now handled through publish/unpublish endpoints
290
- // Users can only update their private watchlist
291
- if (visibility !== undefined && visibility !== 'private') {
292
- return res.status(400).json({
293
- error: "Cannot change visibility directly. Use the publish endpoint to create a public version."
294
- });
295
- }
296
-
297
- if (items !== undefined && existingData.type === 'static') {
298
- updates.items = items;
299
-
300
- // Update global rootdata collection for computation system
301
- // Compare old and new items to find added/removed PIs
302
- const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
303
- const oldItems = existingData.items || [];
304
- const newItems = items || [];
305
- const oldPiCids = new Set(oldItems.map(item => String(item.cid)));
306
- const newPiCids = new Set(newItems.map(item => String(item.cid)));
307
- const today = new Date().toISOString().split('T')[0];
308
- const isPublic = existingData.visibility === 'public';
309
-
310
- // Find added PIs
311
- for (const newItem of newItems) {
312
- const piCid = String(newItem.cid);
313
- if (!oldPiCids.has(piCid)) {
314
- // Update global WatchlistMembershipData/{date} document
315
- await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'add');
316
- // Update PI-centric PopularInvestors/{piCid}/watchlistData
317
- await updatePIWatchlistData(db, logger, piCid, userCid, today, 'add');
318
- }
319
- }
320
-
321
- // Find removed PIs
322
- for (const oldItem of oldItems) {
323
- const piCid = String(oldItem.cid);
324
- if (!newPiCids.has(piCid)) {
325
- // Update global WatchlistMembershipData/{date} document
326
- await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
327
- // Update PI-centric PopularInvestors/{piCid}/watchlistData
328
- await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
329
- }
330
- }
331
- }
332
-
333
- if (dynamicConfig !== undefined && existingData.type === 'dynamic') {
334
- updates.dynamicConfig = dynamicConfig;
335
- }
336
-
337
- // Merge updates with existing data
338
- const updatedData = { ...existingData, ...updates };
339
-
340
- // Write to new SignedInUsers path with dual-write to legacy path
341
- const { writeWithMigration } = require('../core/path_resolution_helpers');
342
- await writeWithMigration(
343
- db,
344
- 'signedInUsers',
345
- 'watchlists',
346
- { cid: userCid },
347
- updatedData,
348
- {
349
- isCollection: true,
350
- merge: true,
351
- dataType: 'watchlists',
352
- config,
353
- collectionRegistry: dependencies.collectionRegistry,
354
- documentId: id,
355
- dualWrite: true
356
- }
357
- );
358
-
359
- logger.log('SUCCESS', `[updateWatchlist] Updated watchlist ${id} for user ${userCid}`);
360
-
361
- return res.status(200).json({
362
- success: true,
363
- watchlist: {
364
- id: id,
365
- ...updatedData
366
- }
367
- });
368
-
369
- } catch (error) {
370
- logger.log('ERROR', `[updateWatchlist] Error updating watchlist ${id} for ${userCid}`, error);
371
- return res.status(500).json({ error: error.message });
372
- }
373
- }
374
-
375
- /**
376
- * DELETE /user/me/watchlists/:id
377
- * Delete a watchlist
378
- */
379
- async function deleteWatchlist(req, res, dependencies, config) {
380
- const { db, logger } = dependencies;
381
- const { userCid } = req.query;
382
- const { id } = req.params;
383
-
384
- if (!userCid || !id) {
385
- return res.status(400).json({ error: "Missing userCid or watchlist id" });
386
- }
387
-
388
- try {
389
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
390
- const watchlistRef = db.collection(watchlistsCollection)
391
- .doc(String(userCid))
392
- .collection('lists')
393
- .doc(id);
394
-
395
- const watchlistDoc = await watchlistRef.get();
396
-
397
- if (!watchlistDoc.exists) {
398
- return res.status(404).json({ error: "Watchlist not found" });
399
- }
400
-
401
- const watchlistData = watchlistDoc.data();
402
-
403
- // Verify ownership
404
- if (watchlistData.createdBy !== Number(userCid)) {
405
- return res.status(403).json({ error: "You can only delete your own watchlists" });
406
- }
407
-
408
- // Update global rootdata collections before deleting
409
- // Remove all PIs from global tracking if this is a static watchlist with items
410
- if (watchlistData.type === 'static' && watchlistData.items && watchlistData.items.length > 0) {
411
- const { updateWatchlistMembershipRootData, updatePIWatchlistData } = require('../rootdata/rootdata_aggregation_helpers');
412
- const today = new Date().toISOString().split('T')[0];
413
- const isPublic = watchlistData.visibility === 'public';
414
-
415
- for (const item of watchlistData.items) {
416
- const piCid = String(item.cid);
417
- // Remove from global WatchlistMembershipData/{date} document
418
- await updateWatchlistMembershipRootData(db, logger, piCid, userCid, isPublic, today, 'remove');
419
- // Remove from PI-centric PopularInvestors/{piCid}/watchlistData
420
- await updatePIWatchlistData(db, logger, piCid, userCid, today, 'remove');
421
- }
422
- }
423
-
424
- // Delete watchlist from both new and legacy paths
425
- const { writeWithMigration } = require('../core/path_resolution_helpers');
426
- // Delete from new path
427
- const newPathRef = db.collection('SignedInUsers')
428
- .doc(String(userCid))
429
- .collection('watchlists')
430
- .doc(id);
431
- await newPathRef.delete().catch(err => {
432
- logger.log('WARN', `[deleteWatchlist] Failed to delete from new path: ${err.message}`);
433
- });
434
-
435
- // Delete from legacy path if it exists
436
- const legacyPathRef = db.collection(config.watchlistsCollection || 'watchlists')
437
- .doc(String(userCid))
438
- .collection('lists')
439
- .doc(id);
440
- await legacyPathRef.delete().catch(err => {
441
- logger.log('WARN', `[deleteWatchlist] Failed to delete from legacy path: ${err.message}`);
442
- });
443
-
444
- // Remove from public watchlists if it was public
445
- if (watchlistData.visibility === 'public') {
446
- const publicRef = db.collection('public_watchlists').doc(id);
447
- await publicRef.delete();
448
- }
449
-
450
- // TODO: Clean up subscriptions for this watchlist
451
- // This would require deleting entries in watchlist_subscriptions collection
452
-
453
- logger.log('SUCCESS', `[deleteWatchlist] Deleted watchlist ${id} for user ${userCid}`);
454
-
455
- return res.status(200).json({
456
- success: true,
457
- message: "Watchlist deleted successfully"
458
- });
459
-
460
- } catch (error) {
461
- logger.log('ERROR', `[deleteWatchlist] Error deleting watchlist ${id} for ${userCid}`, error);
462
- return res.status(500).json({ error: error.message });
463
- }
464
- }
465
-
466
- /**
467
- * POST /user/me/watchlists/:id/copy
468
- * Copy a public watchlist
469
- */
470
- async function copyWatchlist(req, res, dependencies, config) {
471
- const { db, logger } = dependencies;
472
- const { userCid } = req.query;
473
- const { id } = req.params;
474
- const { name, version } = req.body; // Optional custom name and version number
475
-
476
- if (!userCid || !id) {
477
- return res.status(400).json({ error: "Missing userCid or watchlist id" });
478
- }
479
-
480
- const userCidNum = Number(userCid);
481
-
482
- try {
483
- // First, try to find in public watchlists
484
- const publicRef = db.collection('public_watchlists').doc(id);
485
- const publicDoc = await publicRef.get();
486
-
487
- if (!publicDoc.exists) {
488
- return res.status(404).json({ error: "Public watchlist not found" });
489
- }
490
-
491
- const publicData = publicDoc.data();
492
- const originalCreatorCid = Number(publicData.createdBy);
493
-
494
- let originalData;
495
-
496
- // If version is specified, copy from that specific version snapshot
497
- if (version) {
498
- const versionRef = db.collection('public_watchlists')
499
- .doc(id)
500
- .collection('versions')
501
- .doc(String(version));
502
-
503
- const versionDoc = await versionRef.get();
504
-
505
- if (!versionDoc.exists) {
506
- return res.status(404).json({ error: `Version ${version} not found for this watchlist` });
507
- }
508
-
509
- originalData = versionDoc.data();
510
- } else {
511
- // Copy from latest version (get the latest version)
512
- const latestVersion = publicData.latestVersion || 1;
513
- const versionRef = db.collection('public_watchlists')
514
- .doc(id)
515
- .collection('versions')
516
- .doc(String(latestVersion));
517
-
518
- const versionDoc = await versionRef.get();
519
-
520
- if (versionDoc.exists) {
521
- originalData = versionDoc.data();
522
- } else {
523
- // Fallback: try to get from original watchlist (for backwards compatibility)
524
- const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
525
- .doc(String(originalCreatorCid))
526
- .collection('lists')
527
- .doc(id);
528
-
529
- const originalDoc = await originalRef.get();
530
-
531
- if (!originalDoc.exists) {
532
- return res.status(404).json({ error: "Original watchlist not found" });
533
- }
534
-
535
- originalData = originalDoc.data();
536
- }
537
- }
538
-
539
- // Check if user is copying their own watchlist
540
- const isCopyingOwn = originalCreatorCid === userCidNum;
541
-
542
- // If copying own watchlist, find existing copies to determine number
543
- let copyNumber = 1;
544
- let newName = name;
545
-
546
- if (isCopyingOwn) {
547
- // Get all watchlists by this user to find existing copies
548
- const userWatchlistsRef = db.collection(config.watchlistsCollection || 'watchlists')
549
- .doc(String(userCidNum))
550
- .collection('lists');
551
-
552
- const userWatchlistsSnapshot = await userWatchlistsRef.get();
553
- const baseName = originalData.name.replace(/\s*#\d+$/, ''); // Remove existing #N suffix
554
-
555
- // Count existing copies (including original)
556
- const existingCopies = [];
557
- userWatchlistsSnapshot.forEach(doc => {
558
- const data = doc.data();
559
- const docName = data.name.replace(/\s*#\d+$/, '');
560
- if (docName === baseName || docName === originalData.name) {
561
- existingCopies.push(data);
562
- }
563
- });
564
-
565
- // Determine next copy number
566
- copyNumber = existingCopies.length + 1;
567
- newName = name || `${baseName} #${copyNumber}`;
568
- } else {
569
- // Copying someone else's watchlist
570
- newName = name || `${originalData.name} (Copy)`;
571
- }
572
-
573
- // Create new watchlist for the copying user
574
- const newWatchlistId = generateWatchlistId();
575
- const watchlistData = {
576
- ...originalData,
577
- id: newWatchlistId,
578
- name: newName,
579
- createdBy: userCidNum,
580
- visibility: 'private', // Copied watchlists are always private
581
- copiedFrom: id,
582
- copiedFromVersion: version || (publicData.latestVersion || 1), // Track which version was copied
583
- copiedFromCreator: originalCreatorCid,
584
- originalName: originalData.name, // Store original name for comparison
585
- hasBeenModified: false, // Track if user made meaningful changes
586
- createdAt: FieldValue.serverTimestamp(),
587
- updatedAt: FieldValue.serverTimestamp(),
588
- isAutoGenerated: false
589
- };
590
-
591
- // Remove fields that shouldn't be copied
592
- delete watchlistData.copyCount;
593
- delete watchlistData.version;
594
- delete watchlistData.versionId;
595
- delete watchlistData.snapshotAt;
596
- delete watchlistData.isImmutable;
597
- delete watchlistData.watchlistId;
598
-
599
- const newWatchlistRef = db.collection(config.watchlistsCollection || 'watchlists')
600
- .doc(String(userCidNum))
601
- .collection('lists')
602
- .doc(newWatchlistId);
603
-
604
- await newWatchlistRef.set(watchlistData);
605
-
606
- // Increment copy count on original (only if copying someone else's)
607
- if (!isCopyingOwn) {
608
- await publicRef.update({
609
- copyCount: FieldValue.increment(1),
610
- updatedAt: FieldValue.serverTimestamp()
611
- });
612
- }
613
-
614
- logger.log('SUCCESS', `[copyWatchlist] User ${userCid} copied watchlist ${id} as ${newWatchlistId}${isCopyingOwn ? ' (own watchlist)' : ''}`);
615
-
616
- return res.status(201).json({
617
- success: true,
618
- watchlist: {
619
- id: newWatchlistId,
620
- ...watchlistData
621
- }
622
- });
623
-
624
- } catch (error) {
625
- logger.log('ERROR', `[copyWatchlist] Error copying watchlist ${id} for ${userCid}`, error);
626
- return res.status(500).json({ error: error.message });
627
- }
628
- }
629
-
630
- /**
631
- * GET /user/public-watchlists
632
- * Browse public watchlists
633
- */
634
- async function getPublicWatchlists(req, res, dependencies, config) {
635
- const { db, logger } = dependencies;
636
- const { limit = 50, offset = 0 } = req.query;
637
-
638
- try {
639
- const publicRef = db.collection('public_watchlists')
640
- .orderBy('copyCount', 'desc')
641
- .orderBy('createdAt', 'desc')
642
- .limit(parseInt(limit))
643
- .offset(parseInt(offset));
644
-
645
- const snapshot = await publicRef.get();
646
-
647
- const watchlists = [];
648
- snapshot.forEach(doc => {
649
- watchlists.push({
650
- id: doc.id,
651
- ...doc.data()
652
- });
653
- });
654
-
655
- return res.status(200).json({
656
- watchlists,
657
- count: watchlists.length,
658
- limit: parseInt(limit),
659
- offset: parseInt(offset)
660
- });
661
-
662
- } catch (error) {
663
- logger.log('ERROR', `[getPublicWatchlists] Error fetching public watchlists`, error);
664
- return res.status(500).json({ error: error.message });
665
- }
666
- }
667
-
668
- /**
669
- * POST /user/me/watchlists/:id/publish
670
- * Publish a version of a private watchlist
671
- * Creates an immutable snapshot version that can be copied by others
672
- */
673
- async function publishWatchlistVersion(req, res, dependencies, config) {
674
- const { db, logger } = dependencies;
675
- const { userCid } = req.query;
676
- const { id } = req.params;
677
-
678
- if (!userCid || !id) {
679
- return res.status(400).json({ error: "Missing userCid or watchlist id" });
680
- }
681
-
682
- try {
683
- const watchlistsCollection = config.watchlistsCollection || 'watchlists';
684
- const watchlistRef = db.collection(watchlistsCollection)
685
- .doc(String(userCid))
686
- .collection('lists')
687
- .doc(id);
688
-
689
- const watchlistDoc = await watchlistRef.get();
690
-
691
- if (!watchlistDoc.exists) {
692
- return res.status(404).json({ error: "Watchlist not found" });
693
- }
694
-
695
- const watchlistData = watchlistDoc.data();
696
-
697
- // Verify ownership
698
- if (watchlistData.createdBy !== Number(userCid)) {
699
- return res.status(403).json({ error: "You can only publish your own watchlists" });
700
- }
701
-
702
- // Check if this is a copied watchlist that hasn't been modified
703
- if (watchlistData.copiedFrom && watchlistData.copiedFromCreator && !watchlistData.hasBeenModified) {
704
- return res.status(400).json({
705
- error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
706
- });
707
- }
708
-
709
- // Get current version number
710
- const publicRef = db.collection('public_watchlists').doc(id);
711
- const publicDoc = await publicRef.get();
712
-
713
- let versionNumber = 1;
714
- if (publicDoc.exists) {
715
- const publicData = publicDoc.data();
716
- versionNumber = (publicData.latestVersion || 0) + 1;
717
- }
718
-
719
- // Create version snapshot
720
- const versionId = `${id}_v${versionNumber}`;
721
- const versionData = {
722
- watchlistId: id,
723
- version: versionNumber,
724
- createdBy: Number(userCid),
725
- name: watchlistData.name,
726
- type: watchlistData.type,
727
- description: watchlistData.dynamicConfig?.description || '',
728
- snapshotAt: FieldValue.serverTimestamp(),
729
- copyCount: 0,
730
- createdAt: watchlistData.createdAt,
731
- isImmutable: true // Public versions are immutable
732
- };
733
-
734
- // Only include items if it's a static watchlist
735
- if (watchlistData.type === 'static' && watchlistData.items) {
736
- versionData.items = JSON.parse(JSON.stringify(watchlistData.items));
737
- }
738
-
739
- // Only include dynamicConfig if it's a dynamic watchlist
740
- if (watchlistData.type === 'dynamic' && watchlistData.dynamicConfig) {
741
- versionData.dynamicConfig = JSON.parse(JSON.stringify(watchlistData.dynamicConfig));
742
- }
743
-
744
- // Store version in versions subcollection
745
- const versionRef = db.collection('public_watchlists')
746
- .doc(id)
747
- .collection('versions')
748
- .doc(String(versionNumber));
749
-
750
- await versionRef.set(versionData);
751
-
752
- // Update or create public watchlist entry (points to latest version)
753
- await publicRef.set({
754
- watchlistId: id,
755
- createdBy: Number(userCid),
756
- name: watchlistData.name,
757
- type: watchlistData.type,
758
- description: watchlistData.dynamicConfig?.description || '',
759
- latestVersion: versionNumber,
760
- latestVersionId: versionId,
761
- copyCount: publicDoc.exists ? (publicDoc.data().copyCount || 0) : 0,
762
- createdAt: watchlistData.createdAt,
763
- updatedAt: FieldValue.serverTimestamp()
764
- }, { merge: true });
765
-
766
- logger.log('SUCCESS', `[publishWatchlistVersion] User ${userCid} published watchlist ${id} as version ${versionNumber}`);
767
-
768
- return res.status(201).json({
769
- success: true,
770
- version: versionNumber,
771
- versionId: versionId,
772
- watchlist: versionData
773
- });
774
-
775
- } catch (error) {
776
- logger.log('ERROR', `[publishWatchlistVersion] Error publishing watchlist ${id} for ${userCid}`, error);
777
- return res.status(500).json({ error: error.message });
778
- }
779
- }
780
-
781
- /**
782
- * GET /user/public-watchlists/:id/versions
783
- * Get version history for a public watchlist
784
- */
785
- async function getWatchlistVersions(req, res, dependencies, config) {
786
- const { db, logger } = dependencies;
787
- const { id } = req.params;
788
-
789
- try {
790
- const versionsRef = db.collection('public_watchlists')
791
- .doc(id)
792
- .collection('versions')
793
- .orderBy('version', 'desc');
794
-
795
- const snapshot = await versionsRef.get();
796
- const versions = [];
797
-
798
- snapshot.forEach(doc => {
799
- versions.push({
800
- version: doc.data().version,
801
- versionId: `${id}_v${doc.data().version}`,
802
- ...doc.data()
803
- });
804
- });
805
-
806
- return res.status(200).json({
807
- success: true,
808
- watchlistId: id,
809
- versions,
810
- count: versions.length
811
- });
812
-
813
- } catch (error) {
814
- logger.log('ERROR', `[getWatchlistVersions] Error fetching versions for ${id}`, error);
815
- return res.status(500).json({ error: error.message });
816
- }
817
- }
818
-
819
- module.exports = {
820
- getUserWatchlists,
821
- getWatchlist,
822
- createWatchlist,
823
- updateWatchlist,
824
- deleteWatchlist,
825
- copyWatchlist,
826
- getPublicWatchlists,
827
- publishWatchlistVersion,
828
- getWatchlistVersions
829
- };