bulltrackers-module 1.0.504 → 1.0.506

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +345 -0
  2. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +320 -0
  3. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +116 -0
  4. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +171 -0
  5. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +710 -0
  6. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +109 -0
  7. package/functions/generic-api/user-api/MIGRATION_PLAN.md +499 -0
  8. package/functions/generic-api/user-api/README_MIGRATION.md +152 -0
  9. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +106 -0
  10. package/functions/generic-api/user-api/REFACTORING_STATUS.md +85 -0
  11. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +206 -0
  12. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +126 -0
  13. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +327 -0
  14. package/functions/generic-api/user-api/helpers/{test_alert_helpers.js → alerts/test_alert_helpers.js} +1 -1
  15. package/functions/generic-api/user-api/helpers/collection_helpers.js +23 -45
  16. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +68 -0
  17. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +213 -0
  18. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +486 -0
  19. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +77 -0
  20. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +299 -0
  21. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +55 -0
  22. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +238 -0
  23. package/functions/generic-api/user-api/helpers/data/social_helpers.js +55 -0
  24. package/functions/generic-api/user-api/helpers/data_helpers.js +85 -2750
  25. package/functions/generic-api/user-api/helpers/{dev_helpers.js → dev/dev_helpers.js} +0 -1
  26. package/functions/generic-api/user-api/helpers/{on_demand_fetch_helpers.js → fetch/on_demand_fetch_helpers.js} +33 -115
  27. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +360 -0
  28. package/functions/generic-api/user-api/helpers/{notification_helpers.js → notifications/notification_helpers.js} +0 -1
  29. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +200 -0
  30. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +125 -0
  31. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +178 -0
  32. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +65 -0
  33. package/functions/generic-api/user-api/helpers/{review_helpers.js → reviews/review_helpers.js} +23 -107
  34. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +177 -0
  35. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +70 -0
  36. package/functions/generic-api/user-api/helpers/{user_sync_helpers.js → sync/user_sync_helpers.js} +54 -127
  37. package/functions/generic-api/user-api/helpers/{verification_helpers.js → verification/verification_helpers.js} +4 -43
  38. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +95 -0
  39. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +139 -0
  40. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +306 -0
  41. package/functions/generic-api/user-api/helpers/{watchlist_helpers.js → watchlist/watchlist_management_helpers.js} +62 -213
  42. package/functions/generic-api/user-api/index.js +9 -9
  43. package/functions/task-engine/handler_creator.js +7 -6
  44. package/package.json +1 -1
  45. package/functions/generic-api/API_MIGRATION_PLAN.md +0 -436
  46. package/functions/generic-api/user-api/helpers/FALLBACK_CONDITIONS.md +0 -98
  47. package/functions/generic-api/user-api/helpers/HISTORY_STORAGE_LOCATION.md +0 -66
  48. package/functions/generic-api/user-api/helpers/subscription_helpers.js +0 -512
  49. /package/functions/generic-api/user-api/helpers/{alert_helpers.js → alerts/alert_helpers.js} +0 -0
@@ -0,0 +1,486 @@
1
+ /**
2
+ * @fileoverview Path Resolution with Migration Support
3
+ * Enhanced path resolution using collection registry with auto-migration
4
+ * Supports both new CID-based paths and legacy paths with automatic migration
5
+ */
6
+
7
+ const { FieldValue } = require('@google-cloud/firestore');
8
+ const { getCollectionPath, resolvePath, getCollectionMetadata } = require('../../../../../../config/collection_registry');
9
+
10
+ /**
11
+ * Get eToro CID from Firebase UID
12
+ * @param {object} db - Firestore instance
13
+ * @param {string} firebaseUid - Firebase authentication UID
14
+ * @returns {Promise<number|null>} - eToro CID or null if not found
15
+ */
16
+ async function getCidFromFirebaseUid(db, firebaseUid) {
17
+ try {
18
+ const userDoc = await db.collection('signedInUsers').doc(firebaseUid).get();
19
+ if (!userDoc.exists) return null;
20
+ const data = userDoc.data();
21
+ return data.etoroCID || data.cid || null;
22
+ } catch (error) {
23
+ console.error('[getCidFromFirebaseUid] Error fetching CID:', error);
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Get new path from collection registry
30
+ * @param {string} category - Registry category (e.g., 'signedInUsers')
31
+ * @param {string} subcategory - Subcategory name (e.g., 'notifications')
32
+ * @param {object} params - Dynamic segment values (e.g., { cid: '123' })
33
+ * @returns {string} - Resolved path
34
+ */
35
+ function getNewPath(category, subcategory, params = {}) {
36
+ try {
37
+ return getCollectionPath(category, subcategory, params);
38
+ } catch (error) {
39
+ console.error(`[getNewPath] Error resolving path for ${category}/${subcategory}:`, error.message);
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get legacy path mapping for a collection type
46
+ * Maps new collection types to their legacy paths
47
+ * @param {string} dataType - Data type (e.g., 'notifications', 'alerts', 'watchlists')
48
+ * @param {string|number} userCid - User CID
49
+ * @param {object} config - Configuration object
50
+ * @returns {string|null} - Legacy path or null if no legacy path exists
51
+ */
52
+ /**
53
+ * Get legacy path mapping for a collection type
54
+ * Maps new collection types to their legacy paths based on user requirements
55
+ *
56
+ * This function first tries to get legacy paths from the collection registry,
57
+ * then falls back to the hardcoded legacyPathMap for backward compatibility.
58
+ *
59
+ * @param {string} dataType - Data type (e.g., 'notifications', 'alerts', 'watchlists')
60
+ * @param {string|number} userCid - User CID (or piCid for PI-specific data)
61
+ * @param {object} config - Configuration object
62
+ * @param {object} params - Additional parameters (e.g., { firebaseUid, username, date, requestId, etc. })
63
+ * @param {string} category - Registry category (e.g., 'signedInUsers', 'popularInvestors') - optional
64
+ * @param {string} subcategory - Registry subcategory (e.g., 'notifications', 'alerts') - optional
65
+ * @returns {string|null} - Legacy path template or null if no legacy path exists
66
+ */
67
+ function getLegacyPath(dataType, userCid, config = {}, params = {}, category = null, subcategory = null) {
68
+ // Try to get legacy paths from collection registry first
69
+ if (category && subcategory) {
70
+ try {
71
+ const metadata = getCollectionMetadata(category, subcategory);
72
+ if (metadata && metadata.legacyPaths && metadata.legacyPaths.length > 0) {
73
+ // Use first legacy path from registry (can be enhanced to try multiple)
74
+ let legacyPathTemplate = metadata.legacyPaths[0];
75
+
76
+ // Resolve dynamic segments in the legacy path
77
+ const cid = String(userCid);
78
+ const resolvedParams = {
79
+ cid: cid,
80
+ userCid: cid,
81
+ firebaseUid: params.firebaseUid || '',
82
+ username: params.username || '',
83
+ date: params.date || '{date}',
84
+ requestId: params.requestId || '{requestId}',
85
+ piCid: params.piCid || cid,
86
+ reviewId: params.reviewId || `{piCid}_{userCid}`,
87
+ postId: params.postId || '{postId}',
88
+ viewId: params.viewId || `{piCid}_{viewerCid}_{timestamp}`,
89
+ watchlistId: params.watchlistId || '{watchlistId}',
90
+ version: params.version || '{version}',
91
+ ...params // Allow params to override defaults
92
+ };
93
+
94
+ // Resolve path template
95
+ legacyPathTemplate = resolvePath(legacyPathTemplate, resolvedParams);
96
+
97
+ // Replace any remaining placeholders
98
+ for (const [key, value] of Object.entries(resolvedParams)) {
99
+ legacyPathTemplate = legacyPathTemplate.replace(`{${key}}`, String(value));
100
+ }
101
+
102
+ return legacyPathTemplate;
103
+ }
104
+ } catch (error) {
105
+ // Fall through to hardcoded map if registry lookup fails
106
+ console.warn(`[getLegacyPath] Could not get legacy path from registry for ${category}/${subcategory}:`, error.message);
107
+ }
108
+ }
109
+
110
+ // Fallback to hardcoded legacy path map (for backward compatibility)
111
+ const cid = String(userCid);
112
+ const firebaseUid = params.firebaseUid || '';
113
+ const username = params.username || '';
114
+ const date = params.date || '{date}';
115
+ const requestId = params.requestId || '{requestId}';
116
+ const piCid = params.piCid || cid;
117
+ const reviewId = params.reviewId || `{piCid}_{userCid}`;
118
+ const postId = params.postId || '{postId}';
119
+ const viewId = params.viewId || `{piCid}_{viewerCid}_{timestamp}`;
120
+ const watchlistId = params.watchlistId || '{watchlistId}';
121
+ const version = params.version || '{version}';
122
+
123
+ const legacyPathMap = {
124
+ // Signed-in user data (CID-based)
125
+ notifications: firebaseUid ? `user_notifications/${firebaseUid}/notifications` : `user_notifications/${cid}/notifications`,
126
+ notificationsCounters: firebaseUid ? `user_notifications/${firebaseUid}/counters` : `user_notifications/${cid}/counters`,
127
+ alerts: `user_alerts/${cid}/alerts`,
128
+ alertsCounters: `user_alerts/${cid}/counters`,
129
+ watchlists: `user_watchlists/${cid}/lists`,
130
+ subscriptions: `watchlist_subscriptions/${cid}/alerts`,
131
+ verification: config.verificationsCollection ? `${config.verificationsCollection}/${username}` : `user_verifications/${username}`,
132
+ syncRequests: `user_sync_requests/${cid}/requests`,
133
+ syncStatus: `user_sync_requests/${cid}/global/latest`,
134
+ portfolio: config.signedInUsersCollection ? `${config.signedInUsersCollection}/19M/snapshots/${date}/parts` : `signed_in_users/19M/snapshots/${date}/parts`,
135
+ tradeHistory: config.signedInHistoryCollection ? `${config.signedInHistoryCollection}/19M/snapshots/${date}/parts` : `signed_in_user_history/19M/snapshots/${date}/parts`,
136
+ socialPosts: `signed_in_users_social/${cid}/posts`,
137
+
138
+ // Popular Investor data (PI CID-based)
139
+ piFetchRequests: `pi_fetch_requests/${piCid}/requests/${requestId}`,
140
+ piFetchStatus: `pi_fetch_requests/${piCid}/global/latest`,
141
+ piUserFetchRequests: `pi_fetch_requests/${piCid}/user_requests/${cid}`,
142
+ piReviews: `pi_reviews/${reviewId}`,
143
+ piSocialPosts: `pi_social_posts/${piCid}/posts/${postId}`,
144
+ piProfileViews: `profile_views/${piCid}_${date}`,
145
+ piIndividualViews: `profile_views/individual_views/views/${viewId}`,
146
+
147
+ // Public/system data (no migration needed, but documented)
148
+ publicWatchlists: `public_watchlists/${watchlistId}/versions/${version}`,
149
+ userGcidMappings: `user_gcid_mappings/${cid}`,
150
+
151
+ // Root data collections (no migration - these are date-based and stay as-is)
152
+ // These are populated by task engines and should remain in their current format
153
+ signedInUserPortfolioRoot: `SignedInUserPortfolioData/${date}/${cid}`,
154
+ signedInUserTradeHistoryRoot: `SignedInUserTradeHistoryData/${date}/${cid}`,
155
+ signedInUserSocialRoot: `SignedInUserSocialPostData/${date}/${cid}`,
156
+ popularInvestorPortfolioRoot: `PopularInvestorPortfolioData/${date}/${piCid}`,
157
+ popularInvestorTradeHistoryRoot: `PopularInvestorTradeHistoryData/${date}/${piCid}`,
158
+ popularInvestorSocialRoot: `PopularInvestorSocialPostData/${date}/${piCid}`,
159
+ piPortfoliosDeep: `pi_portfolios_deep/19M/snapshots/${date}/parts`,
160
+ piPortfoliosOverall: `pi_portfolios_overall/19M/snapshots/${date}/parts`
161
+ };
162
+
163
+ return legacyPathMap[dataType] || null;
164
+ }
165
+
166
+ /**
167
+ * Read with migration - tries new path first, falls back to legacy, auto-migrates if found
168
+ * @param {object} db - Firestore instance
169
+ * @param {string} category - Registry category
170
+ * @param {string} subcategory - Subcategory name
171
+ * @param {object} params - Path parameters
172
+ * @param {object} options - Read options
173
+ * @param {boolean} options.isCollection - If true, reads as collection; if false, reads as document
174
+ * @param {string} options.dataType - Data type for legacy path lookup
175
+ * @param {object} options.config - Configuration object
176
+ * @param {object} options.logger - Logger instance
177
+ * @param {string} options.documentId - Document ID (for collections)
178
+ * @returns {Promise<object|null>} - Document data or snapshot, with migration info
179
+ */
180
+ async function readWithMigration(db, category, subcategory, params, options = {}) {
181
+ const {
182
+ isCollection = false,
183
+ dataType = null,
184
+ config = {},
185
+ logger = null,
186
+ documentId = null
187
+ } = options;
188
+
189
+ const userCid = params.cid || params.userCid;
190
+
191
+ // Get new path from registry
192
+ let newPath;
193
+ try {
194
+ newPath = getNewPath(category, subcategory, params);
195
+ } catch (error) {
196
+ if (logger) logger.log('WARN', `[readWithMigration] Could not resolve new path for ${category}/${subcategory}: ${error.message}`);
197
+ newPath = null;
198
+ }
199
+
200
+ // Try new path first
201
+ if (newPath) {
202
+ try {
203
+ if (isCollection) {
204
+ const collectionRef = db.collection(newPath);
205
+ const snapshot = await collectionRef.get();
206
+ if (!snapshot.empty) {
207
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${newPath}`);
208
+ return { snapshot, source: 'new', path: newPath };
209
+ }
210
+ } else {
211
+ const docRef = documentId
212
+ ? db.collection(newPath).doc(documentId)
213
+ : db.doc(newPath);
214
+ const doc = await docRef.get();
215
+ if (doc.exists) {
216
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in new path: ${docRef.path}`);
217
+ return { data: doc.data(), source: 'new', path: docRef.path };
218
+ }
219
+ }
220
+ } catch (newError) {
221
+ if (logger) logger.log('WARN', `[readWithMigration] Error reading from new path ${newPath}: ${newError.message}`);
222
+ }
223
+ }
224
+
225
+ // Fallback to legacy path
226
+ if (dataType && userCid) {
227
+ const legacyPathTemplate = getLegacyPath(dataType, userCid, config, params);
228
+ if (legacyPathTemplate) {
229
+ try {
230
+ // Resolve legacy path (may have additional params like date, username)
231
+ // Replace placeholders in legacy path template
232
+ let legacyPath = legacyPathTemplate;
233
+ for (const [key, value] of Object.entries(params)) {
234
+ legacyPath = legacyPath.replace(`{${key}}`, String(value));
235
+ }
236
+ // Also replace common placeholders
237
+ legacyPath = legacyPath.replace(/{date}/g, params.date || '{date}');
238
+ legacyPath = legacyPath.replace(/{requestId}/g, params.requestId || documentId || '{requestId}');
239
+ legacyPath = legacyPath.replace(/{username}/g, params.username || '{username}');
240
+ legacyPath = legacyPath.replace(/{piCid}/g, params.piCid || userCid);
241
+ legacyPath = legacyPath.replace(/{userCid}/g, String(userCid));
242
+ legacyPath = legacyPath.replace(/{cid}/g, String(userCid));
243
+ legacyPath = legacyPath.replace(/{viewerCid}/g, params.viewerCid || '{viewerCid}');
244
+ legacyPath = legacyPath.replace(/{timestamp}/g, params.timestamp || Date.now().toString());
245
+ legacyPath = legacyPath.replace(/{viewId}/g, params.viewId || documentId || '{viewId}');
246
+ legacyPath = legacyPath.replace(/{reviewId}/g, params.reviewId || documentId || `{piCid}_{userCid}`);
247
+ legacyPath = legacyPath.replace(/{postId}/g, params.postId || documentId || '{postId}');
248
+ legacyPath = legacyPath.replace(/{watchlistId}/g, params.watchlistId || '{watchlistId}');
249
+ legacyPath = legacyPath.replace(/{version}/g, params.version || '{version}');
250
+
251
+ if (isCollection) {
252
+ const legacyCollectionRef = db.collection(legacyPath);
253
+ const legacySnapshot = await legacyCollectionRef.get();
254
+ if (!legacySnapshot.empty) {
255
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyPath}, will migrate`);
256
+
257
+ // Auto-migrate if new path is available
258
+ if (newPath) {
259
+ await migrateCollectionData(db, legacySnapshot, newPath, dataType, logger);
260
+ }
261
+
262
+ return { snapshot: legacySnapshot, source: 'legacy', path: legacyPath, migrated: !!newPath };
263
+ }
264
+ } else {
265
+ const legacyDocRef = documentId
266
+ ? db.collection(legacyPath).doc(documentId)
267
+ : db.doc(legacyPath);
268
+ const legacyDoc = await legacyDocRef.get();
269
+ if (legacyDoc.exists) {
270
+ if (logger) logger.log('INFO', `[readWithMigration] Found data in legacy path: ${legacyDocRef.path}, will migrate`);
271
+
272
+ // Auto-migrate if new path is available
273
+ if (newPath) {
274
+ await migrateDocumentData(db, legacyDoc, newPath, documentId, logger);
275
+ }
276
+
277
+ return { data: legacyDoc.data(), source: 'legacy', path: legacyDocRef.path, migrated: !!newPath };
278
+ }
279
+ }
280
+ } catch (legacyError) {
281
+ if (logger) logger.log('WARN', `[readWithMigration] Error reading from legacy path: ${legacyError.message}`);
282
+ }
283
+ }
284
+ }
285
+
286
+ return null;
287
+ }
288
+
289
+ /**
290
+ * Migrate document data from legacy to new path
291
+ * @param {object} db - Firestore instance
292
+ * @param {object} legacyDoc - Legacy document snapshot
293
+ * @param {string} newPath - New path (collection path, not full document path)
294
+ * @param {string} documentId - Document ID
295
+ * @param {object} logger - Logger instance
296
+ * @returns {Promise<void>}
297
+ */
298
+ async function migrateDocumentData(db, legacyDoc, newPath, documentId, logger) {
299
+ try {
300
+ const data = legacyDoc.data();
301
+ const newDocRef = documentId
302
+ ? db.collection(newPath).doc(documentId)
303
+ : db.doc(newPath);
304
+
305
+ // Check if already migrated
306
+ const existingDoc = await newDocRef.get();
307
+ if (existingDoc.exists) {
308
+ if (logger) logger.log('INFO', `[migrateDocumentData] Data already exists in new path: ${newDocRef.path}`);
309
+ return;
310
+ }
311
+
312
+ // Migrate data
313
+ await newDocRef.set({
314
+ ...data,
315
+ _migratedAt: FieldValue.serverTimestamp(),
316
+ _migratedFrom: legacyDoc.ref.path
317
+ });
318
+
319
+ if (logger) logger.log('SUCCESS', `[migrateDocumentData] Migrated document from ${legacyDoc.ref.path} to ${newDocRef.path}`);
320
+ } catch (error) {
321
+ if (logger) logger.log('ERROR', `[migrateDocumentData] Migration failed: ${error.message}`);
322
+ // Don't throw - migration failures shouldn't break reads
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Migrate collection data from legacy to new path
328
+ * @param {object} db - Firestore instance
329
+ * @param {object} legacySnapshot - Legacy collection snapshot
330
+ * @param {string} newPath - New collection path
331
+ * @param {string} dataType - Data type
332
+ * @param {object} logger - Logger instance
333
+ * @returns {Promise<void>}
334
+ */
335
+ async function migrateCollectionData(db, legacySnapshot, newPath, dataType, logger) {
336
+ try {
337
+ const batch = db.batch();
338
+ let migratedCount = 0;
339
+
340
+ for (const doc of legacySnapshot.docs) {
341
+ const newDocRef = db.collection(newPath).doc(doc.id);
342
+
343
+ // Check if already migrated
344
+ const existingDoc = await newDocRef.get();
345
+ if (existingDoc.exists) {
346
+ continue; // Skip if already migrated
347
+ }
348
+
349
+ // Add to batch
350
+ batch.set(newDocRef, {
351
+ ...doc.data(),
352
+ _migratedAt: FieldValue.serverTimestamp(),
353
+ _migratedFrom: doc.ref.path
354
+ });
355
+ migratedCount++;
356
+ }
357
+
358
+ if (migratedCount > 0) {
359
+ await batch.commit();
360
+ if (logger) logger.log('SUCCESS', `[migrateCollectionData] Migrated ${migratedCount} documents from legacy to ${newPath}`);
361
+ }
362
+ } catch (error) {
363
+ if (logger) logger.log('ERROR', `[migrateCollectionData] Migration failed: ${error.message}`);
364
+ // Don't throw - migration failures shouldn't break reads
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Write with dual write support (writes to both new and legacy during migration)
370
+ * @param {object} db - Firestore instance
371
+ * @param {string} category - Registry category
372
+ * @param {string} subcategory - Subcategory name
373
+ * @param {object} params - Path parameters
374
+ * @param {object} data - Data to write
375
+ * @param {object} options - Write options
376
+ * @param {boolean} options.isCollection - If true, writes as collection; if false, writes as document
377
+ * @param {boolean} options.merge - If true, merges data instead of overwriting
378
+ * @param {string} options.dataType - Data type for legacy path lookup
379
+ * @param {object} options.config - Configuration object
380
+ * @param {string} options.documentId - Document ID (required for collections)
381
+ * @param {boolean} options.dualWrite - If true, writes to both paths (default: true during migration)
382
+ * @returns {Promise<void>}
383
+ */
384
+ async function writeWithMigration(db, category, subcategory, params, data, options = {}) {
385
+ const {
386
+ isCollection = false,
387
+ merge = false,
388
+ dataType = null,
389
+ config = {},
390
+ documentId = null,
391
+ dualWrite = true // Default to dual write during migration period
392
+ } = options;
393
+
394
+ const userCid = params.cid || params.userCid;
395
+
396
+ // Get new path
397
+ const newPath = getNewPath(category, subcategory, params);
398
+
399
+ // Get legacy path if dual write is enabled
400
+ let legacyPath = null;
401
+ if (dualWrite && dataType && userCid) {
402
+ legacyPath = getLegacyPath(dataType, userCid, config);
403
+ if (legacyPath) {
404
+ legacyPath = resolvePath(legacyPath, params);
405
+ }
406
+ }
407
+
408
+ const batch = db.batch();
409
+
410
+ try {
411
+ // Write to new path
412
+ if (isCollection) {
413
+ if (!documentId) {
414
+ throw new Error('Collection writes require documentId');
415
+ }
416
+ const newRef = db.collection(newPath).doc(documentId);
417
+ if (merge) {
418
+ batch.set(newRef, data, { merge: true });
419
+ } else {
420
+ batch.set(newRef, data);
421
+ }
422
+ } else {
423
+ const newRef = documentId
424
+ ? db.collection(newPath).doc(documentId)
425
+ : db.doc(newPath);
426
+ if (merge) {
427
+ batch.set(newRef, data, { merge: true });
428
+ } else {
429
+ batch.set(newRef, data);
430
+ }
431
+ }
432
+
433
+ // Write to legacy path if dual write is enabled
434
+ if (legacyPath) {
435
+ if (isCollection) {
436
+ if (!documentId) {
437
+ throw new Error('Collection writes require documentId');
438
+ }
439
+ const legacyRef = db.collection(legacyPath).doc(documentId);
440
+ if (merge) {
441
+ batch.set(legacyRef, data, { merge: true });
442
+ } else {
443
+ batch.set(legacyRef, data);
444
+ }
445
+ } else {
446
+ const legacyRef = documentId
447
+ ? db.collection(legacyPath).doc(documentId)
448
+ : db.doc(legacyPath);
449
+ if (merge) {
450
+ batch.set(legacyRef, data, { merge: true });
451
+ } else {
452
+ batch.set(legacyRef, data);
453
+ }
454
+ }
455
+ }
456
+
457
+ await batch.commit();
458
+ } catch (error) {
459
+ console.error('[writeWithMigration] Error writing data:', error);
460
+ throw error;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Get collection path helper (for backward compatibility)
466
+ * @param {object} collectionRegistry - Collection registry (not used, kept for compatibility)
467
+ * @param {string} category - Registry category
468
+ * @param {string} subcategory - Subcategory name
469
+ * @param {object} params - Path parameters
470
+ * @returns {string} - Resolved path
471
+ */
472
+ function getCollectionPathHelper(collectionRegistry, category, subcategory, params = {}) {
473
+ return getNewPath(category, subcategory, params);
474
+ }
475
+
476
+ module.exports = {
477
+ getCidFromFirebaseUid,
478
+ getNewPath,
479
+ getLegacyPath,
480
+ readWithMigration,
481
+ writeWithMigration,
482
+ migrateDocumentData,
483
+ migrateCollectionData,
484
+ getCollectionPath: getCollectionPathHelper // For backward compatibility
485
+ };
486
+
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @fileoverview User Status Helpers
3
+ * Functions to check user status (PI, signed-in, etc.)
4
+ */
5
+
6
+ const { findLatestRankingsDate } = require('./data_lookup_helpers');
7
+
8
+ /**
9
+ * Check if a signed-in user is also a Popular Investor
10
+ * Returns ranking entry if found, null otherwise
11
+ * Checks dev overrides first for pretendToBePI flag
12
+ * @param {Firestore} db - Firestore instance
13
+ * @param {string|number} userCid - User CID
14
+ * @param {object} config - Configuration object
15
+ * @param {object} logger - Logger instance (optional)
16
+ * @returns {Promise<object|null>} - Ranking entry or null
17
+ */
18
+ async function checkIfUserIsPI(db, userCid, config, logger = null) {
19
+ try {
20
+ // Check dev override first (for developer accounts)
21
+ const { getDevOverride } = require('../dev_helpers');
22
+ const devOverride = await getDevOverride(db, userCid, config, logger);
23
+
24
+ if (devOverride && devOverride.enabled && devOverride.pretendToBePI) {
25
+ // Generate fake ranking entry for dev testing
26
+ const fakeRankEntry = {
27
+ CustomerId: Number(userCid),
28
+ UserName: 'Dev Test PI',
29
+ AUMValue: 500000 + Math.floor(Math.random() * 1000000), // Random AUM between 500k-1.5M
30
+ Copiers: 150 + Math.floor(Math.random() * 200), // Random copiers between 150-350
31
+ RiskScore: 3 + Math.floor(Math.random() * 3), // Random risk score 3-5
32
+ Gain: 25 + Math.floor(Math.random() * 50), // Random gain 25-75%
33
+ WinRatio: 50 + Math.floor(Math.random() * 20), // Random win ratio 50-70%
34
+ Trades: 500 + Math.floor(Math.random() * 1000) // Random trades 500-1500
35
+ };
36
+
37
+ if (logger && logger.log) {
38
+ logger.log('INFO', `[checkIfUserIsPI] DEV OVERRIDE: User ${userCid} pretending to be PI with fake ranking data`);
39
+ } else {
40
+ console.log(`[checkIfUserIsPI] DEV OVERRIDE: User ${userCid} pretending to be PI with fake ranking data`);
41
+ }
42
+
43
+ return fakeRankEntry;
44
+ }
45
+
46
+ // Otherwise, check real rankings
47
+ const rankingsCollection = config.popularInvestorRankingsCollection || process.env.FIRESTORE_COLLECTION_PI_RANKINGS || 'popular_investor_rankings';
48
+ const rankingsDate = await findLatestRankingsDate(db, rankingsCollection, 30);
49
+
50
+ if (!rankingsDate) {
51
+ return null;
52
+ }
53
+
54
+ const rankingsRef = db.collection(rankingsCollection).doc(rankingsDate);
55
+ const rankingsDoc = await rankingsRef.get();
56
+
57
+ if (!rankingsDoc.exists) {
58
+ return null;
59
+ }
60
+
61
+ const rankingsData = rankingsDoc.data();
62
+ const rankingsItems = rankingsData.Items || [];
63
+
64
+ // Find user in rankings
65
+ const userRankEntry = rankingsItems.find(item => String(item.CustomerId) === String(userCid));
66
+
67
+ return userRankEntry || null;
68
+ } catch (error) {
69
+ console.error('[checkIfUserIsPI] Error checking if user is PI:', error);
70
+ return null;
71
+ }
72
+ }
73
+
74
+ module.exports = {
75
+ checkIfUserIsPI
76
+ };
77
+