bulltrackers-module 1.0.501 → 1.0.502

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.
@@ -89,13 +89,10 @@ async function requestPiFetch(req, res, dependencies, config) {
89
89
  const requestId = `req_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
90
90
  const now = new Date();
91
91
 
92
- // Store request
93
- const requestRef = db.collection('pi_fetch_requests')
94
- .doc(String(piCidNum))
95
- .collection('requests')
96
- .doc(requestId);
92
+ const { getCollectionPath, writeDual, extractCollectionName } = require('./collection_helpers');
93
+ const { collectionRegistry } = dependencies;
97
94
 
98
- await requestRef.set({
95
+ const requestData = {
99
96
  requestId,
100
97
  userCid: requestUserCid, // Use effective CID
101
98
  actualUserCid: Number(userCid), // Track actual developer CID for audit
@@ -105,35 +102,92 @@ async function requestPiFetch(req, res, dependencies, config) {
105
102
  isImpersonating: isImpersonating || false,
106
103
  createdAt: FieldValue.serverTimestamp(),
107
104
  updatedAt: FieldValue.serverTimestamp()
108
- });
105
+ };
109
106
 
110
- // Update user rate limit (use effective CID)
111
- const userRequestRef = db.collection('pi_fetch_requests')
112
- .doc(String(piCidNum))
113
- .collection('user_requests')
114
- .doc(String(requestUserCid));
107
+ // Store request in new structure: popularInvestors/{piCid}/fetchRequests/{requestId}
108
+ if (collectionRegistry) {
109
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchRequests', {
110
+ piCid: String(piCidNum)
111
+ }) + `/${requestId}`;
112
+
113
+ const legacyPath = `pi_fetch_requests/${piCidNum}/requests/${requestId}`;
114
+
115
+ await writeDual(db, newPath, legacyPath, requestData);
116
+ } else {
117
+ // Fallback to legacy only
118
+ const requestRef = db.collection('pi_fetch_requests')
119
+ .doc(String(piCidNum))
120
+ .collection('requests')
121
+ .doc(requestId);
122
+
123
+ await requestRef.set(requestData);
124
+ }
115
125
 
116
- await userRequestRef.set({
126
+ // Update user rate limit (use effective CID)
127
+ const userRequestData = {
117
128
  userCid: requestUserCid,
118
129
  piCid: piCidNum,
119
130
  lastRequestedAt: FieldValue.serverTimestamp(),
120
131
  requestCount: FieldValue.increment(1),
121
132
  updatedAt: FieldValue.serverTimestamp()
122
- }, { merge: true });
133
+ };
123
134
 
124
- // Update global rate limit
125
- const globalRef = db.collection('pi_fetch_requests')
126
- .doc(String(piCidNum))
127
- .collection('global')
128
- .doc('latest');
135
+ if (collectionRegistry) {
136
+ const newUserPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'userFetchRequests', {
137
+ piCid: String(piCidNum)
138
+ }) + `/${requestUserCid}`;
139
+
140
+ const legacyUserPath = `pi_fetch_requests/${piCidNum}/user_requests/${requestUserCid}`;
141
+
142
+ await writeDual(db, newUserPath, legacyUserPath, userRequestData, { merge: true });
143
+ } else {
144
+ const userRequestRef = db.collection('pi_fetch_requests')
145
+ .doc(String(piCidNum))
146
+ .collection('user_requests')
147
+ .doc(String(requestUserCid));
148
+
149
+ await userRequestRef.set(userRequestData, { merge: true });
150
+ }
129
151
 
130
- await globalRef.set({
152
+ // Update global rate limit
153
+ const globalData = {
131
154
  piCid: piCidNum,
132
155
  lastRequestedAt: FieldValue.serverTimestamp(),
133
156
  lastRequestedBy: requestUserCid, // Use effective CID
134
157
  totalRequests: FieldValue.increment(1),
135
158
  updatedAt: FieldValue.serverTimestamp()
136
- }, { merge: true });
159
+ };
160
+
161
+ if (collectionRegistry) {
162
+ const newGlobalPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchStatus', {
163
+ piCid: String(piCidNum)
164
+ });
165
+
166
+ const legacyGlobalPath = `pi_fetch_requests/${piCidNum}/global/latest`;
167
+
168
+ await writeDual(db, newGlobalPath, legacyGlobalPath, globalData, { merge: true });
169
+ } else {
170
+ const globalRef = db.collection('pi_fetch_requests')
171
+ .doc(String(piCidNum))
172
+ .collection('global')
173
+ .doc('latest');
174
+
175
+ await globalRef.set(globalData, { merge: true });
176
+ }
177
+
178
+ // Get request ref for later updates
179
+ let requestRef;
180
+ if (collectionRegistry) {
181
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchRequests', {
182
+ piCid: String(piCidNum)
183
+ }) + `/${requestId}`;
184
+ requestRef = db.doc(newPath);
185
+ } else {
186
+ requestRef = db.collection('pi_fetch_requests')
187
+ .doc(String(piCidNum))
188
+ .collection('requests')
189
+ .doc(requestId);
190
+ }
137
191
 
138
192
  // Publish to task engine - use on-demand topic for API requests
139
193
  const topicName = config.taskEngine?.PUBSUB_TOPIC_USER_FETCH_ONDEMAND ||
@@ -268,13 +322,41 @@ async function getPiFetchStatus(req, res, dependencies, config) {
268
322
  }
269
323
 
270
324
  // Data doesn't exist, check for pending requests
271
- const requestsRef = db.collection('pi_fetch_requests')
272
- .doc(String(piCidNum))
273
- .collection('requests')
274
- .orderBy('createdAt', 'desc')
275
- .limit(1);
325
+ const { getCollectionPath, extractCollectionName, readWithFallback } = require('./collection_helpers');
326
+ const { collectionRegistry } = dependencies;
276
327
 
277
- const requestsSnapshot = await requestsRef.get();
328
+ let requestsSnapshot = null;
329
+
330
+ // Try new structure first
331
+ if (collectionRegistry) {
332
+ try {
333
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'fetchRequests', {
334
+ piCid: String(piCidNum)
335
+ });
336
+ const collectionName = extractCollectionName(newPath);
337
+
338
+ const requestsRef = db.collection(collectionName)
339
+ .doc(String(piCidNum))
340
+ .collection('fetchRequests')
341
+ .orderBy('createdAt', 'desc')
342
+ .limit(1);
343
+
344
+ requestsSnapshot = await requestsRef.get();
345
+ } catch (newError) {
346
+ logger.log('WARN', `[getPiFetchStatus] New structure failed, trying legacy: ${newError.message}`);
347
+ }
348
+ }
349
+
350
+ // Fallback to legacy structure
351
+ if (!requestsSnapshot || requestsSnapshot.empty) {
352
+ const requestsRef = db.collection('pi_fetch_requests')
353
+ .doc(String(piCidNum))
354
+ .collection('requests')
355
+ .orderBy('createdAt', 'desc')
356
+ .limit(1);
357
+
358
+ requestsSnapshot = await requestsRef.get();
359
+ }
278
360
 
279
361
  if (!requestsSnapshot.empty) {
280
362
  const latestRequest = requestsSnapshot.docs[0].data();
@@ -321,8 +321,33 @@ async function submitReview(req, res, dependencies, config) {
321
321
 
322
322
  // 3. Check if review already exists (for update) - use effective CID for review ID
323
323
  const reviewId = `${effectiveCid}_${piCid}`;
324
- const existingReview = await db.collection(reviewsCollection).doc(reviewId).get();
325
- const isUpdate = existingReview.exists;
324
+
325
+ const { getCollectionPath, writeDual, readWithFallback } = require('./collection_helpers');
326
+ const { collectionRegistry } = dependencies;
327
+
328
+ // Check existing review in new or legacy structure
329
+ let existingReview = null;
330
+ let isUpdate = false;
331
+
332
+ if (collectionRegistry) {
333
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
334
+ piCid: String(piCid)
335
+ }) + `/${reviewId}`;
336
+
337
+ const legacyPath = `${reviewsCollection}/${reviewId}`;
338
+
339
+ const result = await readWithFallback(db, newPath, legacyPath);
340
+ if (result && result.data) {
341
+ existingReview = { exists: true, data: () => result.data };
342
+ isUpdate = true;
343
+ }
344
+ }
345
+
346
+ // Fallback to legacy check
347
+ if (!existingReview) {
348
+ existingReview = await db.collection(reviewsCollection).doc(reviewId).get();
349
+ isUpdate = existingReview.exists;
350
+ }
326
351
 
327
352
  // 4. Store/Update Review (store effective CID, but track actual CID for audit)
328
353
  const reviewData = {
@@ -341,7 +366,18 @@ async function submitReview(req, res, dependencies, config) {
341
366
  reviewData.createdAt = FieldValue.serverTimestamp();
342
367
  }
343
368
 
344
- await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
369
+ // Write to both new and legacy structures
370
+ if (collectionRegistry) {
371
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
372
+ piCid: String(piCid)
373
+ }) + `/${reviewId}`;
374
+
375
+ const legacyPath = `${reviewsCollection}/${reviewId}`;
376
+
377
+ await writeDual(db, newPath, legacyPath, reviewData, { merge: true });
378
+ } else {
379
+ await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
380
+ }
345
381
 
346
382
  logger.log('INFO', `[Review] User ${userCid} ${isUpdate ? 'updated' : 'submitted'} review for PI ${piCid}`);
347
383
  return res.status(200).json({
@@ -368,13 +404,39 @@ async function getReviews(req, res, dependencies, config) {
368
404
 
369
405
  try {
370
406
  const piCidNum = Number(piCid);
407
+ const { getCollectionPath, extractCollectionName } = require('./collection_helpers');
408
+ const { collectionRegistry } = dependencies;
371
409
 
372
- // Query reviews for this PI
373
- const snapshot = await db.collection(reviewsCollection)
374
- .where('piCid', '==', piCidNum)
375
- .orderBy('createdAt', 'desc')
376
- .limit(100) // Increased limit
377
- .get();
410
+ let snapshot = null;
411
+
412
+ // Try new structure first
413
+ if (collectionRegistry) {
414
+ try {
415
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
416
+ piCid: String(piCidNum)
417
+ });
418
+ const collectionName = extractCollectionName(newPath);
419
+
420
+ const reviewsRef = db.collection(collectionName)
421
+ .doc(String(piCidNum))
422
+ .collection('reviews')
423
+ .orderBy('createdAt', 'desc')
424
+ .limit(100);
425
+
426
+ snapshot = await reviewsRef.get();
427
+ } catch (newError) {
428
+ logger.log('WARN', `[getReviews] New structure failed, trying legacy: ${newError.message}`);
429
+ }
430
+ }
431
+
432
+ // Fallback to legacy structure
433
+ if (!snapshot || snapshot.empty) {
434
+ snapshot = await db.collection(reviewsCollection)
435
+ .where('piCid', '==', piCidNum)
436
+ .orderBy('createdAt', 'desc')
437
+ .limit(100)
438
+ .get();
439
+ }
378
440
 
379
441
  const reviews = [];
380
442
  let totalStars = 0;
@@ -448,14 +510,36 @@ async function getUserReview(req, res, dependencies, config) {
448
510
  }
449
511
 
450
512
  try {
513
+ const { getCollectionPath, readWithFallback } = require('./collection_helpers');
514
+ const { collectionRegistry } = dependencies;
451
515
  const reviewId = `${userCid}_${piCid}`;
452
- const reviewDoc = await db.collection(reviewsCollection).doc(reviewId).get();
453
-
454
- if (!reviewDoc.exists) {
455
- return res.status(200).json({ exists: false, review: null });
516
+
517
+ let reviewData = null;
518
+
519
+ // Try new structure first
520
+ if (collectionRegistry) {
521
+ const newPath = getCollectionPath(collectionRegistry, 'popularInvestors', 'reviews', {
522
+ piCid: String(piCid)
523
+ }) + `/${reviewId}`;
524
+
525
+ const legacyPath = `${reviewsCollection}/${reviewId}`;
526
+
527
+ const result = await readWithFallback(db, newPath, legacyPath);
528
+ if (result && result.data) {
529
+ reviewData = result.data;
530
+ }
456
531
  }
457
-
458
- const data = reviewDoc.data();
532
+
533
+ // Fallback to legacy
534
+ if (!reviewData) {
535
+ const reviewDoc = await db.collection(reviewsCollection).doc(reviewId).get();
536
+ if (!reviewDoc.exists) {
537
+ return res.status(200).json({ exists: false, review: null });
538
+ }
539
+ reviewData = reviewDoc.data();
540
+ }
541
+
542
+ const data = reviewData;
459
543
  const review = {
460
544
  id: reviewDoc.id,
461
545
  rating: data.rating,
@@ -67,14 +67,41 @@ async function subscribeToAlerts(req, res, dependencies, config) {
67
67
  lastAlertAt: null
68
68
  };
69
69
 
70
- // Store subscription
71
- const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
72
- const subscriptionRef = db.collection(subscriptionsCollection)
73
- .doc(String(userCid))
74
- .collection('alerts')
75
- .doc(String(piCid));
76
-
77
- await subscriptionRef.set(subscriptionData, { merge: true });
70
+ const { getCollectionPath, writeDual } = require('./collection_helpers');
71
+ const { collectionRegistry } = dependencies;
72
+
73
+ // Determine storage location based on whether watchlistId is present
74
+ if (watchlistId && collectionRegistry) {
75
+ // Per-watchlist subscription: signedInUsers/{cid}/watchlists/{watchlistId}/subscriptions/{piCid}
76
+ const newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlistSubscriptions', {
77
+ cid: String(userCid),
78
+ watchlistId: watchlistId
79
+ }) + `/${piCid}`;
80
+
81
+ const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
82
+
83
+ await writeDual(db, newPath, legacyPath, subscriptionData, { merge: true });
84
+ } else {
85
+ // Global subscription: signedInUsers/{cid}/subscriptions/{piCid}
86
+ if (collectionRegistry) {
87
+ const newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
88
+ cid: String(userCid)
89
+ }) + `/${piCid}`;
90
+
91
+ const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
92
+
93
+ await writeDual(db, newPath, legacyPath, subscriptionData, { merge: true });
94
+ } else {
95
+ // Fallback to legacy only
96
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
97
+ const subscriptionRef = db.collection(subscriptionsCollection)
98
+ .doc(String(userCid))
99
+ .collection('alerts')
100
+ .doc(String(piCid));
101
+
102
+ await subscriptionRef.set(subscriptionData, { merge: true });
103
+ }
104
+ }
78
105
 
79
106
  logger.log('SUCCESS', `[subscribeToAlerts] User ${userCid} subscribed to alerts for PI ${piCid} in watchlist ${watchlistId}`);
80
107
 
@@ -104,44 +131,72 @@ async function updateSubscription(req, res, dependencies, config) {
104
131
  }
105
132
 
106
133
  try {
107
- const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
108
- const subscriptionRef = db.collection(subscriptionsCollection)
109
- .doc(String(userCid))
110
- .collection('alerts')
111
- .doc(String(piCid));
112
-
113
- const subscriptionDoc = await subscriptionRef.get();
134
+ const { getCollectionPath, readWithFallback, writeDual } = require('./collection_helpers');
135
+ const { collectionRegistry } = dependencies;
136
+
137
+ // Try to find subscription in new structure first, then legacy
138
+ let subscriptionData = null;
139
+ let subscriptionRef = null;
140
+ let newPath = null;
141
+ let legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
142
+
143
+ // Check global subscriptions first
144
+ if (collectionRegistry) {
145
+ newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
146
+ cid: String(userCid)
147
+ }) + `/${piCid}`;
148
+
149
+ const result = await readWithFallback(db, newPath, legacyPath);
150
+ if (result && result.data) {
151
+ subscriptionData = result.data;
152
+ }
153
+ }
114
154
 
115
- if (!subscriptionDoc.exists) {
116
- return res.status(404).json({ error: "Subscription not found" });
155
+ // Fallback to legacy if not found
156
+ if (!subscriptionData) {
157
+ const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
158
+ subscriptionRef = db.collection(subscriptionsCollection)
159
+ .doc(String(userCid))
160
+ .collection('alerts')
161
+ .doc(String(piCid));
162
+
163
+ const subscriptionDoc = await subscriptionRef.get();
164
+ if (!subscriptionDoc.exists) {
165
+ return res.status(404).json({ error: "Subscription not found" });
166
+ }
167
+ subscriptionData = subscriptionDoc.data();
117
168
  }
118
169
 
119
170
  const updates = {};
120
-
121
171
  if (alertTypes !== undefined) {
122
172
  updates.alertTypes = alertTypes;
123
173
  }
124
-
125
174
  if (thresholds !== undefined) {
126
175
  updates.thresholds = thresholds;
127
176
  }
128
-
129
177
  if (Object.keys(updates).length === 0) {
130
178
  return res.status(400).json({ error: "No updates provided" });
131
179
  }
132
180
 
133
181
  updates.updatedAt = FieldValue.serverTimestamp();
134
182
 
135
- await subscriptionRef.update(updates);
183
+ // Update both structures if using new path, otherwise just legacy
184
+ if (newPath && collectionRegistry) {
185
+ const updatedData = { ...subscriptionData, ...updates };
186
+ await writeDual(db, newPath, legacyPath, updatedData, { merge: true });
187
+ } else if (subscriptionRef) {
188
+ await subscriptionRef.update(updates);
189
+ }
136
190
 
137
191
  logger.log('SUCCESS', `[updateSubscription] Updated subscription for user ${userCid}, PI ${piCid}`);
138
192
 
139
- const updatedDoc = await subscriptionRef.get();
193
+ // Return updated data
194
+ const finalData = { ...subscriptionData, ...updates };
140
195
  return res.status(200).json({
141
196
  success: true,
142
197
  subscription: {
143
- id: updatedDoc.id,
144
- ...updatedDoc.data()
198
+ piCid: Number(piCid),
199
+ ...finalData
145
200
  }
146
201
  });
147
202
 
@@ -165,6 +220,54 @@ async function unsubscribeFromAlerts(req, res, dependencies, config) {
165
220
  }
166
221
 
167
222
  try {
223
+ const { getCollectionPath, readWithFallback } = require('./collection_helpers');
224
+ const { collectionRegistry } = dependencies;
225
+
226
+ const legacyPath = `watchlist_subscriptions/${userCid}/alerts/${piCid}`;
227
+ let newPath = null;
228
+ let found = false;
229
+
230
+ // Check global subscriptions first
231
+ if (collectionRegistry) {
232
+ newPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
233
+ cid: String(userCid)
234
+ }) + `/${piCid}`;
235
+
236
+ const newDoc = await db.doc(newPath).get();
237
+ if (newDoc.exists) {
238
+ await db.doc(newPath).delete();
239
+ found = true;
240
+ }
241
+ }
242
+
243
+ // Also check per-watchlist subscriptions (need to search all watchlists)
244
+ if (collectionRegistry && !found) {
245
+ const { getCollectionPath } = require('./collection_helpers');
246
+ const watchlistsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlists', {
247
+ cid: String(userCid)
248
+ });
249
+ const watchlistsRef = db.collection(watchlistsPath.split('/')[0])
250
+ .doc(String(userCid))
251
+ .collection('watchlists');
252
+
253
+ const watchlistsSnapshot = await watchlistsRef.get();
254
+ for (const watchlistDoc of watchlistsSnapshot.docs) {
255
+ const watchlistId = watchlistDoc.id;
256
+ const watchlistSubPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlistSubscriptions', {
257
+ cid: String(userCid),
258
+ watchlistId: watchlistId
259
+ }) + `/${piCid}`;
260
+
261
+ const subDoc = await db.doc(watchlistSubPath).get();
262
+ if (subDoc.exists) {
263
+ await db.doc(watchlistSubPath).delete();
264
+ found = true;
265
+ break;
266
+ }
267
+ }
268
+ }
269
+
270
+ // Delete from legacy structure
168
271
  const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
169
272
  const subscriptionRef = db.collection(subscriptionsCollection)
170
273
  .doc(String(userCid))
@@ -172,13 +275,15 @@ async function unsubscribeFromAlerts(req, res, dependencies, config) {
172
275
  .doc(String(piCid));
173
276
 
174
277
  const subscriptionDoc = await subscriptionRef.get();
278
+ if (subscriptionDoc.exists) {
279
+ await subscriptionRef.delete();
280
+ found = true;
281
+ }
175
282
 
176
- if (!subscriptionDoc.exists) {
283
+ if (!found) {
177
284
  return res.status(404).json({ error: "Subscription not found" });
178
285
  }
179
286
 
180
- await subscriptionRef.delete();
181
-
182
287
  logger.log('SUCCESS', `[unsubscribeFromAlerts] User ${userCid} unsubscribed from alerts for PI ${piCid}`);
183
288
 
184
289
  return res.status(200).json({
@@ -205,19 +310,99 @@ async function getUserSubscriptions(req, res, dependencies, config) {
205
310
  }
206
311
 
207
312
  try {
313
+ const { getCollectionPath, extractCollectionName } = require('./collection_helpers');
314
+ const { collectionRegistry } = dependencies;
315
+
316
+ const subscriptions = [];
317
+ const seenPiCids = new Set();
318
+
319
+ // 1. Read from global subscriptions (new structure)
320
+ if (collectionRegistry) {
321
+ try {
322
+ const globalSubsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'subscriptions', {
323
+ cid: String(userCid)
324
+ });
325
+ const collectionName = extractCollectionName(globalSubsPath);
326
+
327
+ const globalSubsRef = db.collection(collectionName)
328
+ .doc(String(userCid))
329
+ .collection('subscriptions');
330
+
331
+ const globalSnapshot = await globalSubsRef.get();
332
+ globalSnapshot.forEach(doc => {
333
+ const data = doc.data();
334
+ subscriptions.push({
335
+ piCid: Number(doc.id),
336
+ ...data
337
+ });
338
+ seenPiCids.add(Number(doc.id));
339
+ });
340
+ } catch (newError) {
341
+ logger.log('WARN', `[getUserSubscriptions] Failed to read global subscriptions from new structure: ${newError.message}`);
342
+ }
343
+
344
+ // 2. Read from per-watchlist subscriptions
345
+ try {
346
+ const watchlistsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlists', {
347
+ cid: String(userCid)
348
+ });
349
+ const watchlistsCollectionName = extractCollectionName(watchlistsPath);
350
+
351
+ const watchlistsRef = db.collection(watchlistsCollectionName)
352
+ .doc(String(userCid))
353
+ .collection('watchlists');
354
+
355
+ const watchlistsSnapshot = await watchlistsRef.get();
356
+
357
+ for (const watchlistDoc of watchlistsSnapshot.docs) {
358
+ const watchlistId = watchlistDoc.id;
359
+ const watchlistSubsPath = getCollectionPath(collectionRegistry, 'signedInUsers', 'watchlistSubscriptions', {
360
+ cid: String(userCid),
361
+ watchlistId: watchlistId
362
+ });
363
+ const watchlistSubsCollectionName = extractCollectionName(watchlistSubsPath);
364
+
365
+ const watchlistSubsRef = db.collection(watchlistSubsCollectionName)
366
+ .doc(String(userCid))
367
+ .collection('watchlists')
368
+ .doc(watchlistId)
369
+ .collection('subscriptions');
370
+
371
+ const watchlistSubsSnapshot = await watchlistSubsRef.get();
372
+ watchlistSubsSnapshot.forEach(doc => {
373
+ const piCid = Number(doc.id);
374
+ if (!seenPiCids.has(piCid)) {
375
+ const data = doc.data();
376
+ subscriptions.push({
377
+ piCid,
378
+ watchlistId,
379
+ ...data
380
+ });
381
+ seenPiCids.add(piCid);
382
+ }
383
+ });
384
+ }
385
+ } catch (watchlistError) {
386
+ logger.log('WARN', `[getUserSubscriptions] Failed to read watchlist subscriptions: ${watchlistError.message}`);
387
+ }
388
+ }
389
+
390
+ // 3. Fallback to legacy structure for any missing subscriptions
208
391
  const subscriptionsCollection = config.watchlistSubscriptionsCollection || 'watchlist_subscriptions';
209
392
  const subscriptionsRef = db.collection(subscriptionsCollection)
210
393
  .doc(String(userCid))
211
394
  .collection('alerts');
212
395
 
213
- const snapshot = await subscriptionsRef.get();
214
-
215
- const subscriptions = [];
216
- snapshot.forEach(doc => {
217
- subscriptions.push({
218
- piCid: Number(doc.id),
219
- ...doc.data()
220
- });
396
+ const legacySnapshot = await subscriptionsRef.get();
397
+ legacySnapshot.forEach(doc => {
398
+ const piCid = Number(doc.id);
399
+ if (!seenPiCids.has(piCid)) {
400
+ subscriptions.push({
401
+ piCid,
402
+ ...doc.data()
403
+ });
404
+ seenPiCids.add(piCid);
405
+ }
221
406
  });
222
407
 
223
408
  return res.status(200).json({