bulltrackers-module 1.0.464 → 1.0.466

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.
@@ -1,5 +1,5 @@
1
- # Data Feeder Pipeline (Final Verified Version)
2
- # Orchestrates data fetching with UTC-alignment, Test Mode, and extended Timeouts.
1
+ # Data Feeder Pipeline (V2.3 - Try/Retry Syntax Fixed)
2
+ # Orchestrates data fetching with UTC-alignment, Test Mode, and Reliability.
3
3
 
4
4
  main:
5
5
  params: [input]
@@ -9,9 +9,16 @@ main:
9
9
  - project: '${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}'
10
10
  - location: "europe-west1"
11
11
  - market_date: '${text.split(time.format(sys.now()), "T")[0]}'
12
+ # Define a central retry policy to reuse across all HTTP calls
13
+ - default_retry:
14
+ predicate: ${http.default_retry_predicate}
15
+ max_retries: 5
16
+ backoff:
17
+ initial_delay: 2
18
+ max_delay: 60
19
+ multiplier: 2
12
20
 
13
21
  # --- TEST MODE / SELECTIVE EXECUTION ---
14
- # Static routing to allow manual testing of specific steps via Input JSON.
15
22
  - check_test_mode:
16
23
  switch:
17
24
  - condition: '${input != null and "target_step" in input}'
@@ -34,33 +41,36 @@ main:
34
41
  - price_fetch:
35
42
  steps:
36
43
  - call_price_fetcher:
37
- call: http.post
38
- args:
39
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
40
- auth: { type: OIDC }
41
- timeout: 300 # Fixed: Prevents timeout errors for long-running functions
42
- retry: ${http.default_retry}
44
+ try:
45
+ call: http.post
46
+ args:
47
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/price-fetcher"}'
48
+ auth: { type: OIDC }
49
+ timeout: 300
50
+ retry: ${default_retry} # Fixed: Moved retry to a Try Step
43
51
  - insights_fetch:
44
52
  steps:
45
53
  - call_insights_fetcher:
46
- call: http.post
47
- args:
48
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
49
- auth: { type: OIDC }
50
- timeout: 300 # Fixed: Prevents timeout errors for long-running functions
51
- retry: ${http.default_retry}
54
+ try:
55
+ call: http.post
56
+ args:
57
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/insights-fetcher"}'
58
+ auth: { type: OIDC }
59
+ timeout: 300
60
+ retry: ${default_retry} # Fixed: Moved retry to a Try Step
52
61
 
53
62
  - index_market_data:
54
- call: http.post
55
- args:
56
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
57
- body:
58
- targetDate: '${market_date}'
59
- auth: { type: OIDC }
60
- timeout: 300
63
+ try:
64
+ call: http.post
65
+ args:
66
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
67
+ body:
68
+ targetDate: '${market_date}'
69
+ auth: { type: OIDC }
70
+ timeout: 300
71
+ retry: ${default_retry}
61
72
 
62
73
  # --- PHASE 2: ALIGN TO MIDNIGHT ---
63
- # Dynamically calculates seconds remaining until exactly 00:00 UTC.
64
74
  - wait_for_midnight:
65
75
  assign:
66
76
  - now_sec: '${int(sys.now())}'
@@ -79,6 +89,7 @@ main:
79
89
  url: '${"https://" + location + "-" + project + ".cloudfunctions.net/fetch-popular-investors"}'
80
90
  auth: { type: OIDC }
81
91
  timeout: 300
92
+ retry: ${default_retry}
82
93
  except:
83
94
  as: e
84
95
  steps:
@@ -89,31 +100,36 @@ main:
89
100
  text: '${"Rankings Fetch Failed: " + json.encode(e)}'
90
101
 
91
102
  - run_social_midnight:
92
- call: http.post
93
- args:
94
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
95
- auth: { type: OIDC }
96
- timeout: 300
103
+ try:
104
+ call: http.post
105
+ args:
106
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
107
+ auth: { type: OIDC }
108
+ timeout: 300
109
+ retry: ${default_retry}
97
110
 
98
- # Fixed: Split assign and call into two separate steps
99
111
  - prepare_midnight_index:
100
112
  assign:
101
113
  - current_date: '${text.split(time.format(sys.now()), "T")[0]}'
102
114
  - index_midnight_data:
103
- call: http.post
104
- args:
105
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
106
- body:
107
- targetDate: '${current_date}'
108
- auth: { type: OIDC }
109
- timeout: 300
115
+ try:
116
+ call: http.post
117
+ args:
118
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
119
+ body:
120
+ targetDate: '${current_date}'
121
+ auth: { type: OIDC }
122
+ timeout: 300
123
+ retry: ${default_retry}
110
124
 
111
125
  - run_global_indexer:
112
- call: http.post
113
- args:
114
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
115
- auth: { type: OIDC }
116
- timeout: 300
126
+ try:
127
+ call: http.post
128
+ args:
129
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
130
+ auth: { type: OIDC }
131
+ timeout: 300
132
+ retry: ${default_retry}
117
133
 
118
134
  # --- PHASE 4: RECURRING SOCIAL FETCH (UTC Aligned 3hr) ---
119
135
  - init_social_loop:
@@ -127,7 +143,7 @@ main:
127
143
  - calculate_next_window:
128
144
  assign:
129
145
  - now_sec_loop: '${int(sys.now())}'
130
- - window_size: 10800 # Fixed: Aligns to 03:00, 06:00, 09:00 UTC...
146
+ - window_size: 10800
131
147
  - sleep_loop: '${window_size - (now_sec_loop % window_size)}'
132
148
 
133
149
  - wait_for_3hr_window:
@@ -136,24 +152,27 @@ main:
136
152
  seconds: '${sleep_loop}'
137
153
 
138
154
  - run_social_recurring:
139
- call: http.post
140
- args:
141
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
142
- auth: { type: OIDC }
143
- timeout: 300
155
+ try:
156
+ call: http.post
157
+ args:
158
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/social-orchestrator"}'
159
+ auth: { type: OIDC }
160
+ timeout: 300
161
+ retry: ${default_retry}
144
162
 
145
- # Fixed: Split assign and call into two separate steps
146
163
  - prepare_recurring_index:
147
164
  assign:
148
165
  - cur_date_rec: '${text.split(time.format(sys.now()), "T")[0]}'
149
166
  - index_recurring:
150
- call: http.post
151
- args:
152
- url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
153
- body:
154
- targetDate: '${cur_date_rec}'
155
- auth: { type: OIDC }
156
- timeout: 300
167
+ try:
168
+ call: http.post
169
+ args:
170
+ url: '${"https://" + location + "-" + project + ".cloudfunctions.net/root-data-indexer"}'
171
+ body:
172
+ targetDate: '${cur_date_rec}'
173
+ auth: { type: OIDC }
174
+ timeout: 300
175
+ retry: ${default_retry}
157
176
 
158
177
  - increment:
159
178
  assign:
@@ -20,7 +20,8 @@ async function getAlertTypes(req, res, dependencies, config) {
20
20
  id: type.id,
21
21
  name: type.name,
22
22
  description: type.description,
23
- severity: type.severity
23
+ severity: type.severity,
24
+ computationName: type.computationName // Include computation name for dynamic watchlists
24
25
  }))
25
26
  });
26
27
  } catch (error) {
@@ -30,6 +31,33 @@ async function getAlertTypes(req, res, dependencies, config) {
30
31
  }
31
32
  }
32
33
 
34
+ /**
35
+ * GET /user/me/dynamic-watchlist-computations
36
+ * Get available computations for dynamic watchlists (from alert types)
37
+ */
38
+ async function getDynamicWatchlistComputations(req, res, dependencies, config) {
39
+ try {
40
+ const alertTypes = getAllAlertTypes();
41
+
42
+ // Extract unique computations from alert types
43
+ const computations = alertTypes.map(type => ({
44
+ computationName: type.computationName,
45
+ alertTypeName: type.name,
46
+ description: type.description,
47
+ severity: type.severity
48
+ }));
49
+
50
+ return res.status(200).json({
51
+ success: true,
52
+ computations
53
+ });
54
+ } catch (error) {
55
+ const { logger } = dependencies;
56
+ logger.log('ERROR', '[getDynamicWatchlistComputations] Error fetching computations', error);
57
+ return res.status(500).json({ error: error.message });
58
+ }
59
+ }
60
+
33
61
  /**
34
62
  * GET /user/me/alerts
35
63
  * Get user's alerts (paginated)
@@ -317,6 +345,7 @@ async function deleteAlert(req, res, dependencies, config) {
317
345
 
318
346
  module.exports = {
319
347
  getAlertTypes,
348
+ getDynamicWatchlistComputations,
320
349
  getUserAlerts,
321
350
  getAlertCount,
322
351
  markAlertRead,
@@ -148,20 +148,7 @@ async function createWatchlist(req, res, dependencies, config) {
148
148
 
149
149
  await watchlistRef.set(watchlistData);
150
150
 
151
- // If public, also add to public watchlists collection
152
- if (visibility === 'public') {
153
- const publicRef = db.collection('public_watchlists').doc(watchlistId);
154
- await publicRef.set({
155
- watchlistId,
156
- createdBy: Number(userCid),
157
- name: watchlistData.name,
158
- type,
159
- description: dynamicConfig?.description || '',
160
- copyCount: 0,
161
- createdAt: FieldValue.serverTimestamp(),
162
- updatedAt: FieldValue.serverTimestamp()
163
- });
164
- }
151
+ // Watchlists are always created as private - no public version on creation
165
152
 
166
153
  logger.log('SUCCESS', `[createWatchlist] Created ${type} watchlist "${name}" for user ${userCid}`);
167
154
 
@@ -250,29 +237,12 @@ async function updateWatchlist(req, res, dependencies, config) {
250
237
  updates.hasBeenModified = true;
251
238
  }
252
239
 
253
- if (visibility !== undefined) {
254
- if (visibility !== 'public' && visibility !== 'private') {
255
- return res.status(400).json({ error: "Visibility must be 'public' or 'private'" });
256
- }
257
- updates.visibility = visibility;
258
-
259
- // Update public watchlists collection
260
- const publicRef = db.collection('public_watchlists').doc(id);
261
- if (visibility === 'public') {
262
- await publicRef.set({
263
- watchlistId: id,
264
- createdBy: existingData.createdBy,
265
- name: updates.name || existingData.name,
266
- type: existingData.type,
267
- description: (dynamicConfig?.description || existingData.dynamicConfig?.description || ''),
268
- copyCount: existingData.copyCount || 0,
269
- createdAt: existingData.createdAt,
270
- updatedAt: FieldValue.serverTimestamp()
271
- }, { merge: true });
272
- } else {
273
- // Remove from public if making private
274
- await publicRef.delete();
275
- }
240
+ // Visibility changes are now handled through publish/unpublish endpoints
241
+ // Users can only update their private watchlist
242
+ if (visibility !== undefined && visibility !== 'private') {
243
+ return res.status(400).json({
244
+ error: "Cannot change visibility directly. Use the publish endpoint to create a public version."
245
+ });
276
246
  }
277
247
 
278
248
  if (items !== undefined && existingData.type === 'static') {
@@ -368,7 +338,7 @@ async function copyWatchlist(req, res, dependencies, config) {
368
338
  const { db, logger } = dependencies;
369
339
  const { userCid } = req.query;
370
340
  const { id } = req.params;
371
- const { name } = req.body; // Optional custom name
341
+ const { name, version } = req.body; // Optional custom name and version number
372
342
 
373
343
  if (!userCid || !id) {
374
344
  return res.status(400).json({ error: "Missing userCid or watchlist id" });
@@ -388,20 +358,51 @@ async function copyWatchlist(req, res, dependencies, config) {
388
358
  const publicData = publicDoc.data();
389
359
  const originalCreatorCid = Number(publicData.createdBy);
390
360
 
391
- // Find the original watchlist
392
- const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
393
- .doc(String(originalCreatorCid))
394
- .collection('lists')
395
- .doc(id);
396
-
397
- const originalDoc = await originalRef.get();
361
+ let originalData;
398
362
 
399
- if (!originalDoc.exists) {
400
- return res.status(404).json({ error: "Original watchlist not found" });
363
+ // If version is specified, copy from that specific version snapshot
364
+ if (version) {
365
+ const versionRef = db.collection('public_watchlists')
366
+ .doc(id)
367
+ .collection('versions')
368
+ .doc(String(version));
369
+
370
+ const versionDoc = await versionRef.get();
371
+
372
+ if (!versionDoc.exists) {
373
+ return res.status(404).json({ error: `Version ${version} not found for this watchlist` });
374
+ }
375
+
376
+ originalData = versionDoc.data();
377
+ } else {
378
+ // Copy from latest version (get the latest version)
379
+ const latestVersion = publicData.latestVersion || 1;
380
+ const versionRef = db.collection('public_watchlists')
381
+ .doc(id)
382
+ .collection('versions')
383
+ .doc(String(latestVersion));
384
+
385
+ const versionDoc = await versionRef.get();
386
+
387
+ if (versionDoc.exists) {
388
+ originalData = versionDoc.data();
389
+ } else {
390
+ // Fallback: try to get from original watchlist (for backwards compatibility)
391
+ const originalRef = db.collection(config.watchlistsCollection || 'watchlists')
392
+ .doc(String(originalCreatorCid))
393
+ .collection('lists')
394
+ .doc(id);
395
+
396
+ const originalDoc = await originalRef.get();
397
+
398
+ if (!originalDoc.exists) {
399
+ return res.status(404).json({ error: "Original watchlist not found" });
400
+ }
401
+
402
+ originalData = originalDoc.data();
403
+ }
401
404
  }
402
405
 
403
- const originalData = originalDoc.data();
404
-
405
406
  // Check if user is copying their own watchlist
406
407
  const isCopyingOwn = originalCreatorCid === userCidNum;
407
408
 
@@ -445,6 +446,7 @@ async function copyWatchlist(req, res, dependencies, config) {
445
446
  createdBy: userCidNum,
446
447
  visibility: 'private', // Copied watchlists are always private
447
448
  copiedFrom: id,
449
+ copiedFromVersion: version || (publicData.latestVersion || 1), // Track which version was copied
448
450
  copiedFromCreator: originalCreatorCid,
449
451
  originalName: originalData.name, // Store original name for comparison
450
452
  hasBeenModified: false, // Track if user made meaningful changes
@@ -455,6 +457,11 @@ async function copyWatchlist(req, res, dependencies, config) {
455
457
 
456
458
  // Remove fields that shouldn't be copied
457
459
  delete watchlistData.copyCount;
460
+ delete watchlistData.version;
461
+ delete watchlistData.versionId;
462
+ delete watchlistData.snapshotAt;
463
+ delete watchlistData.isImmutable;
464
+ delete watchlistData.watchlistId;
458
465
 
459
466
  const newWatchlistRef = db.collection(config.watchlistsCollection || 'watchlists')
460
467
  .doc(String(userCidNum))
@@ -525,6 +532,150 @@ async function getPublicWatchlists(req, res, dependencies, config) {
525
532
  }
526
533
  }
527
534
 
535
+ /**
536
+ * POST /user/me/watchlists/:id/publish
537
+ * Publish a version of a private watchlist
538
+ * Creates an immutable snapshot version that can be copied by others
539
+ */
540
+ async function publishWatchlistVersion(req, res, dependencies, config) {
541
+ const { db, logger } = dependencies;
542
+ const { userCid } = req.query;
543
+ const { id } = req.params;
544
+
545
+ if (!userCid || !id) {
546
+ return res.status(400).json({ error: "Missing userCid or watchlist id" });
547
+ }
548
+
549
+ try {
550
+ const watchlistsCollection = config.watchlistsCollection || 'watchlists';
551
+ const watchlistRef = db.collection(watchlistsCollection)
552
+ .doc(String(userCid))
553
+ .collection('lists')
554
+ .doc(id);
555
+
556
+ const watchlistDoc = await watchlistRef.get();
557
+
558
+ if (!watchlistDoc.exists) {
559
+ return res.status(404).json({ error: "Watchlist not found" });
560
+ }
561
+
562
+ const watchlistData = watchlistDoc.data();
563
+
564
+ // Verify ownership
565
+ if (watchlistData.createdBy !== Number(userCid)) {
566
+ return res.status(403).json({ error: "You can only publish your own watchlists" });
567
+ }
568
+
569
+ // Check if this is a copied watchlist that hasn't been modified
570
+ if (watchlistData.copiedFrom && watchlistData.copiedFromCreator && !watchlistData.hasBeenModified) {
571
+ return res.status(400).json({
572
+ error: "Cannot publish copied watchlist without making meaningful changes. Please modify the watchlist items, thresholds, or parameters before publishing."
573
+ });
574
+ }
575
+
576
+ // Get current version number
577
+ const publicRef = db.collection('public_watchlists').doc(id);
578
+ const publicDoc = await publicRef.get();
579
+
580
+ let versionNumber = 1;
581
+ if (publicDoc.exists) {
582
+ const publicData = publicDoc.data();
583
+ versionNumber = (publicData.latestVersion || 0) + 1;
584
+ }
585
+
586
+ // Create version snapshot
587
+ const versionId = `${id}_v${versionNumber}`;
588
+ const versionData = {
589
+ watchlistId: id,
590
+ version: versionNumber,
591
+ createdBy: Number(userCid),
592
+ name: watchlistData.name,
593
+ type: watchlistData.type,
594
+ description: watchlistData.dynamicConfig?.description || '',
595
+ // Snapshot the current state
596
+ items: watchlistData.items ? JSON.parse(JSON.stringify(watchlistData.items)) : undefined,
597
+ dynamicConfig: watchlistData.dynamicConfig ? JSON.parse(JSON.stringify(watchlistData.dynamicConfig)) : undefined,
598
+ snapshotAt: FieldValue.serverTimestamp(),
599
+ copyCount: 0,
600
+ createdAt: watchlistData.createdAt,
601
+ isImmutable: true // Public versions are immutable
602
+ };
603
+
604
+ // Store version in versions subcollection
605
+ const versionRef = db.collection('public_watchlists')
606
+ .doc(id)
607
+ .collection('versions')
608
+ .doc(String(versionNumber));
609
+
610
+ await versionRef.set(versionData);
611
+
612
+ // Update or create public watchlist entry (points to latest version)
613
+ await publicRef.set({
614
+ watchlistId: id,
615
+ createdBy: Number(userCid),
616
+ name: watchlistData.name,
617
+ type: watchlistData.type,
618
+ description: watchlistData.dynamicConfig?.description || '',
619
+ latestVersion: versionNumber,
620
+ latestVersionId: versionId,
621
+ copyCount: publicDoc.exists ? (publicDoc.data().copyCount || 0) : 0,
622
+ createdAt: watchlistData.createdAt,
623
+ updatedAt: FieldValue.serverTimestamp()
624
+ }, { merge: true });
625
+
626
+ logger.log('SUCCESS', `[publishWatchlistVersion] User ${userCid} published watchlist ${id} as version ${versionNumber}`);
627
+
628
+ return res.status(201).json({
629
+ success: true,
630
+ version: versionNumber,
631
+ versionId: versionId,
632
+ watchlist: versionData
633
+ });
634
+
635
+ } catch (error) {
636
+ logger.log('ERROR', `[publishWatchlistVersion] Error publishing watchlist ${id} for ${userCid}`, error);
637
+ return res.status(500).json({ error: error.message });
638
+ }
639
+ }
640
+
641
+ /**
642
+ * GET /user/public-watchlists/:id/versions
643
+ * Get version history for a public watchlist
644
+ */
645
+ async function getWatchlistVersions(req, res, dependencies, config) {
646
+ const { db, logger } = dependencies;
647
+ const { id } = req.params;
648
+
649
+ try {
650
+ const versionsRef = db.collection('public_watchlists')
651
+ .doc(id)
652
+ .collection('versions')
653
+ .orderBy('version', 'desc');
654
+
655
+ const snapshot = await versionsRef.get();
656
+ const versions = [];
657
+
658
+ snapshot.forEach(doc => {
659
+ versions.push({
660
+ version: doc.data().version,
661
+ versionId: `${id}_v${doc.data().version}`,
662
+ ...doc.data()
663
+ });
664
+ });
665
+
666
+ return res.status(200).json({
667
+ success: true,
668
+ watchlistId: id,
669
+ versions,
670
+ count: versions.length
671
+ });
672
+
673
+ } catch (error) {
674
+ logger.log('ERROR', `[getWatchlistVersions] Error fetching versions for ${id}`, error);
675
+ return res.status(500).json({ error: error.message });
676
+ }
677
+ }
678
+
528
679
  module.exports = {
529
680
  getUserWatchlists,
530
681
  getWatchlist,
@@ -532,6 +683,8 @@ module.exports = {
532
683
  updateWatchlist,
533
684
  deleteWatchlist,
534
685
  copyWatchlist,
535
- getPublicWatchlists
686
+ getPublicWatchlists,
687
+ publishWatchlistVersion,
688
+ getWatchlistVersions
536
689
  };
537
690
 
@@ -6,10 +6,10 @@ const express = require('express');
6
6
  const { submitReview, getReviews, getUserReview, checkReviewEligibility } = require('./helpers/review_helpers');
7
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
- const { getUserWatchlists, getWatchlist: getWatchlistById, createWatchlist, updateWatchlist: updateWatchlistById, deleteWatchlist, copyWatchlist, getPublicWatchlists } = require('./helpers/watchlist_helpers');
9
+ const { getUserWatchlists, getWatchlist: getWatchlistById, createWatchlist, updateWatchlist: updateWatchlistById, deleteWatchlist, copyWatchlist, getPublicWatchlists, publishWatchlistVersion, getWatchlistVersions } = require('./helpers/watchlist_helpers');
10
10
  const { subscribeToAlerts, updateSubscription, unsubscribeFromAlerts, getUserSubscriptions, subscribeToWatchlist } = require('./helpers/subscription_helpers');
11
11
  const { setDevOverride, getDevOverrideStatus } = require('./helpers/dev_helpers');
12
- const { getAlertTypes, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
12
+ const { getAlertTypes, getDynamicWatchlistComputations, getUserAlerts, getAlertCount, markAlertRead, markAllAlertsRead, deleteAlert } = require('./helpers/alert_helpers');
13
13
  const { requestPiFetch, getPiFetchStatus } = require('./helpers/on_demand_fetch_helpers');
14
14
  const { requestUserSync, getUserSyncStatus } = require('./helpers/user_sync_helpers');
15
15
 
@@ -65,9 +65,11 @@ module.exports = (dependencies, config) => {
65
65
  router.put('/me/watchlists/:id', (req, res) => updateWatchlistById(req, res, dependencies, config));
66
66
  router.delete('/me/watchlists/:id', (req, res) => deleteWatchlist(req, res, dependencies, config));
67
67
  router.post('/me/watchlists/:id/copy', (req, res) => copyWatchlist(req, res, dependencies, config));
68
+ router.post('/me/watchlists/:id/publish', (req, res) => publishWatchlistVersion(req, res, dependencies, config));
68
69
 
69
70
  // Public watchlists
70
71
  router.get('/public-watchlists', (req, res) => getPublicWatchlists(req, res, dependencies, config));
72
+ router.get('/public-watchlists/:id/versions', (req, res) => getWatchlistVersions(req, res, dependencies, config));
71
73
 
72
74
  // --- Alert Subscriptions ---
73
75
  router.post('/me/subscriptions', (req, res) => subscribeToAlerts(req, res, dependencies, config));
@@ -88,6 +90,7 @@ module.exports = (dependencies, config) => {
88
90
 
89
91
  // --- Alert Management ---
90
92
  router.get('/me/alert-types', (req, res) => getAlertTypes(req, res, dependencies, config));
93
+ router.get('/me/dynamic-watchlist-computations', (req, res) => getDynamicWatchlistComputations(req, res, dependencies, config));
91
94
  router.get('/me/alerts', (req, res) => getUserAlerts(req, res, dependencies, config));
92
95
  router.get('/me/alerts/count', (req, res) => getAlertCount(req, res, dependencies, config));
93
96
  router.put('/me/alerts/:alertId/read', (req, res) => markAlertRead(req, res, dependencies, config));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.464",
3
+ "version": "1.0.466",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [