bulltrackers-module 1.0.591 → 1.0.592

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +6 -6
  2. package/functions/alert-system/index.js +1 -1
  3. package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
  4. package/functions/api-v2/helpers/task_engine_helper.js +51 -0
  5. package/functions/api-v2/index.js +36 -0
  6. package/functions/api-v2/middleware/identity_middleware.js +48 -0
  7. package/functions/api-v2/package.json +6 -0
  8. package/functions/api-v2/routes/alerts.js +168 -0
  9. package/functions/api-v2/routes/index.js +35 -0
  10. package/functions/api-v2/routes/notifications.js +38 -0
  11. package/functions/api-v2/routes/popular_investors.js +204 -0
  12. package/functions/api-v2/routes/profile.js +212 -0
  13. package/functions/api-v2/routes/reviews.js +72 -0
  14. package/functions/api-v2/routes/settings.js +71 -0
  15. package/functions/api-v2/routes/sync.js +132 -0
  16. package/functions/api-v2/routes/verification.js +47 -0
  17. package/functions/api-v2/routes/watchlists.js +148 -0
  18. package/functions/computation-system/helpers/computation_worker.js +1 -1
  19. package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
  20. package/index.js +6 -2
  21. package/package.json +2 -1
  22. package/functions/generic-api/admin-api/index.js +0 -895
  23. package/functions/generic-api/helpers/api_helpers.js +0 -457
  24. package/functions/generic-api/index.js +0 -204
  25. package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
  26. package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
  27. package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
  28. package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
  29. package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
  30. package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
  31. package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
  32. package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
  33. package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
  34. package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
  35. package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
  36. package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
  37. package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
  38. package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
  39. package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
  40. package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
  41. package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
  42. package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
  43. package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
  44. package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
  45. package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
  46. package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
  47. package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
  48. package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
  49. package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
  50. package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
  51. package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
  52. package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
  53. package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
  54. package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
  55. package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
  56. package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
  57. package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
  58. package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
  59. package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
  60. package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
  61. package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
  62. package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
  63. package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
  64. package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
  65. package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
  66. package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
  67. package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
  68. package/functions/generic-api/user-api/index.js +0 -109
@@ -1,550 +0,0 @@
1
- /**
2
- * @fileoverview Helpers for the Review System.
3
- * Validates copy history before allowing reviews.
4
- */
5
-
6
- const { FieldValue } = require('@google-cloud/firestore');
7
- const zlib = require('zlib');
8
-
9
- /**
10
- * Helper to decompress computation results
11
- */
12
- function tryDecompress(data) {
13
- if (data && data._compressed === true && data.payload) {
14
- try {
15
- let buffer;
16
- if (Buffer.isBuffer(data.payload)) {
17
- buffer = data.payload;
18
- } else if (typeof data.payload === 'string') {
19
- try {
20
- buffer = Buffer.from(data.payload, 'base64');
21
- } catch (e) {
22
- try {
23
- return JSON.parse(data.payload);
24
- } catch (e2) {
25
- buffer = Buffer.from(data.payload);
26
- }
27
- }
28
- } else {
29
- buffer = Buffer.from(data.payload);
30
- }
31
- const decompressed = zlib.gunzipSync(buffer);
32
- const jsonString = decompressed.toString('utf8');
33
- const parsed = JSON.parse(jsonString);
34
- if (typeof parsed === 'string') {
35
- return JSON.parse(parsed);
36
- }
37
- return parsed;
38
- } catch (e) {
39
- console.error('[ReviewHelpers] Decompression failed:', e.message);
40
- return {};
41
- }
42
- }
43
- return data;
44
- }
45
-
46
- /**
47
- * Finds the latest computation date for a given computation
48
- */
49
- async function findLatestComputationDate(db, insightsCollection, resultsSub, compsSub, category, computationName, userCid, maxDays = 30) {
50
- const today = new Date();
51
- for (let i = 0; i < maxDays; i++) {
52
- const checkDate = new Date(today);
53
- checkDate.setDate(today.getDate() - i);
54
- const dateStr = checkDate.toISOString().split('T')[0];
55
-
56
- const computationRef = db.collection(insightsCollection)
57
- .doc(dateStr)
58
- .collection(resultsSub)
59
- .doc(category)
60
- .collection(compsSub)
61
- .doc(computationName);
62
-
63
- const doc = await computationRef.get();
64
- if (doc.exists) {
65
- console.log(`[findLatestComputationDate] Found computation ${computationName} in category ${category} for date ${dateStr}`);
66
- return dateStr;
67
- }
68
- }
69
- console.log(`[findLatestComputationDate] No computation found for ${computationName} in category ${category} within ${maxDays} days`);
70
- return null;
71
- }
72
-
73
- /**
74
- * Checks if a Signed-In User has ever copied a specific PI.
75
- * Uses dev override (if enabled) > SignedInUserCopiedPIs computation > direct portfolio check.
76
- */
77
- async function hasUserCopied(db, userCid, piCid, config) {
78
- const { signedInUsersCollection } = config;
79
- const piCidNum = Number(piCid);
80
- const userCidNum = Number(userCid);
81
-
82
- try {
83
- // === DEV OVERRIDE CHECK (for developer accounts) ===
84
- const { hasUserCopiedWithDevOverride } = require('../dev/dev_helpers');
85
- const devResult = await hasUserCopiedWithDevOverride(db, userCid, piCid, config, console);
86
- if (devResult !== null) {
87
- // devResult is true/false (not null), meaning dev override is active
88
- return devResult;
89
- }
90
- // devResult is null, meaning no dev override, continue with normal logic
91
-
92
- // === PRIMARY: Try to fetch from computation results ===
93
- const insightsCollection = config.unifiedInsightsCollection || 'unified_insights';
94
- const resultsSub = config.resultsSubcollection || 'results';
95
- const compsSub = config.computationsSubcollection || 'computations';
96
-
97
- // Try both computation names and categories
98
- const computationConfigs = [
99
- { category: 'signed_in_user', name: 'SignedInUserCopiedPIs', format: 'object' }, // Returns { current: [], past: [], all: [] }
100
- { category: 'signed_in_user', name: 'SignedInUserCopiedList', format: 'array' }, // Returns array directly
101
- { category: 'popular-investor', name: 'SignedInUserCopiedList', format: 'array' }, // Alternative location
102
- ];
103
-
104
- for (const configItem of computationConfigs) {
105
- const computationDate = await findLatestComputationDate(
106
- db,
107
- insightsCollection,
108
- resultsSub,
109
- compsSub,
110
- configItem.category,
111
- configItem.name,
112
- userCidNum,
113
- 30
114
- );
115
-
116
- if (computationDate) {
117
- const computationRef = db.collection(insightsCollection)
118
- .doc(computationDate)
119
- .collection(resultsSub)
120
- .doc(configItem.category)
121
- .collection(compsSub)
122
- .doc(configItem.name);
123
-
124
- const computationDoc = await computationRef.get();
125
-
126
- if (computationDoc.exists) {
127
- const rawData = computationDoc.data();
128
-
129
- // Handle sharded data
130
- let mergedData = null;
131
- if (rawData._sharded === true && rawData._shardCount) {
132
- // Data is in shards - read all shards and merge
133
- const shardsCol = computationRef.collection('_shards');
134
- const shardsSnapshot = await shardsCol.get();
135
-
136
- if (!shardsSnapshot.empty) {
137
- mergedData = {};
138
- for (const shardDoc of shardsSnapshot.docs) {
139
- const shardData = shardDoc.data();
140
- // Merge shard data - each shard may contain different user IDs
141
- for (const [key, value] of Object.entries(shardData)) {
142
- if (mergedData[key]) {
143
- // If key exists, merge arrays or objects
144
- if (Array.isArray(mergedData[key]) && Array.isArray(value)) {
145
- mergedData[key] = [...mergedData[key], ...value];
146
- } else if (typeof mergedData[key] === 'object' && typeof value === 'object') {
147
- mergedData[key] = { ...mergedData[key], ...value };
148
- } else {
149
- mergedData[key] = value;
150
- }
151
- } else {
152
- mergedData[key] = value;
153
- }
154
- }
155
- }
156
- }
157
- } else {
158
- // Data is in main document - decompress if needed
159
- const { tryDecompress } = require('../data_helpers');
160
- mergedData = tryDecompress(rawData);
161
-
162
- // Handle string decompression result
163
- if (typeof mergedData === 'string') {
164
- try {
165
- mergedData = JSON.parse(mergedData);
166
- } catch (e) {
167
- console.error(`[hasUserCopied] Failed to parse decompressed string:`, e.message);
168
- continue;
169
- }
170
- }
171
- }
172
-
173
- if (!mergedData) continue;
174
-
175
- // Extract user data based on format
176
- const userData = mergedData[String(userCidNum)];
177
- if (!userData) continue;
178
-
179
- if (configItem.format === 'object') {
180
- // SignedInUserCopiedPIs format: { userId: { current: [], past: [], all: [] } }
181
- if (userData.all && Array.isArray(userData.all)) {
182
- if (userData.all.includes(piCidNum)) {
183
- console.log(`[hasUserCopied] Found PI ${piCidNum} in SignedInUserCopiedPIs.all for user ${userCid} (date: ${computationDate}, category: ${configItem.category})`);
184
- return true;
185
- }
186
- }
187
- } else if (configItem.format === 'array') {
188
- // SignedInUserCopiedList format: { userId: [{ cid: X, ... }, ...] }
189
- if (Array.isArray(userData)) {
190
- const hasCopied = userData.some(item => Number(item.cid) === piCidNum);
191
- if (hasCopied) {
192
- console.log(`[hasUserCopied] Found PI ${piCidNum} in SignedInUserCopiedList for user ${userCid} (date: ${computationDate}, category: ${configItem.category})`);
193
- return true;
194
- }
195
- }
196
- }
197
- }
198
- }
199
- }
200
-
201
- // === FALLBACK: Check Active Mirrors (Portfolio) directly ===
202
- const userDoc = await db.collection(signedInUsersCollection).doc(String(userCid)).get();
203
- if (userDoc.exists) {
204
- const data = userDoc.data();
205
-
206
- // Check AggregatedMirrors (current copies)
207
- if (data.AggregatedMirrors && Array.isArray(data.AggregatedMirrors)) {
208
- const isCopying = data.AggregatedMirrors.some(m => Number(m.ParentCID) === piCidNum);
209
- if (isCopying) {
210
- console.log(`[hasUserCopied] Found PI ${piCidNum} in AggregatedMirrors for user ${userCid}`);
211
- return true;
212
- }
213
- }
214
-
215
- // Also check historical mirrors if available
216
- if (data.snapshots && typeof data.snapshots === 'object') {
217
- // Check recent snapshots for any copy history
218
- const snapshotDates = Object.keys(data.snapshots).sort().reverse().slice(0, 30); // Last 30 days
219
- for (const date of snapshotDates) {
220
- const snapshot = data.snapshots[date];
221
- if (snapshot && snapshot.AggregatedMirrors && Array.isArray(snapshot.AggregatedMirrors)) {
222
- const wasCopying = snapshot.AggregatedMirrors.some(m => Number(m.ParentCID) === piCidNum);
223
- if (wasCopying) {
224
- console.log(`[hasUserCopied] Found PI ${piCidNum} in historical snapshot ${date} for user ${userCid}`);
225
- return true;
226
- }
227
- }
228
- }
229
- }
230
- }
231
-
232
- console.log(`[hasUserCopied] User ${userCid} has not copied PI ${piCidNum} (checked computation and portfolio)`);
233
- return false;
234
- } catch (error) {
235
- console.error('[hasUserCopied] Error checking copy status:', error);
236
- // Fallback to false on error
237
- return false;
238
- }
239
- }
240
-
241
- /**
242
- * Gets username for a user CID
243
- */
244
- async function getUsername(db, userCid, config) {
245
- try {
246
- const userDoc = await db.collection(config.signedInUsersCollection).doc(String(userCid)).get();
247
- if (userDoc.exists) {
248
- const data = userDoc.data();
249
- return data.username || null;
250
- }
251
- } catch (error) {
252
- console.error('[getUsername] Error fetching username:', error);
253
- }
254
- return null;
255
- }
256
-
257
- /**
258
- * POST /review
259
- * Submit or update a review for a Popular Investor
260
- */
261
- async function submitReview(req, res, dependencies, config) {
262
- const { db, logger } = dependencies;
263
- const { userCid, piCid, rating, comment, isAnonymous } = req.body;
264
- const { reviewsCollection } = config;
265
-
266
- if (!userCid || !piCid || !rating) {
267
- return res.status(400).json({ error: "Missing required fields (userCid, piCid, rating)." });
268
- }
269
-
270
- // Validate rating
271
- const ratingNum = Number(rating);
272
- if (isNaN(ratingNum) || ratingNum < 1 || ratingNum > 5) {
273
- return res.status(400).json({ error: "Rating must be between 1 and 5." });
274
- }
275
-
276
- try {
277
- // Check for dev override impersonation
278
- const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
279
- const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
280
- const devOverride = await getDevOverride(db, userCid, config, logger);
281
- const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
282
-
283
- // Block self-reviews
284
- if (Number(effectiveCid) === Number(piCid)) {
285
- return res.status(400).json({
286
- error: "Cannot review own profile",
287
- reason: 'self_review'
288
- });
289
- }
290
-
291
- // Check if the effective user is a Popular Investor
292
- const { checkIfUserIsPI } = require('../data_helpers');
293
- const rankEntry = await checkIfUserIsPI(db, effectiveCid, config, logger);
294
-
295
- // Block Popular Investors from reviewing other Popular Investors
296
- if (rankEntry) {
297
- const targetRankEntry = await checkIfUserIsPI(db, Number(piCid), config, logger);
298
- if (targetRankEntry) {
299
- return res.status(400).json({
300
- error: "Popular Investors cannot review other Popular Investors",
301
- reason: 'pi_to_pi_review'
302
- });
303
- }
304
- }
305
-
306
- // 1. Validate "Verified Copier" Status (use effective CID)
307
- const canReview = await hasUserCopied(db, effectiveCid, piCid, config);
308
-
309
- if (!canReview) {
310
- return res.status(403).json({
311
- error: "Verification Failed",
312
- message: "You must have copied this Popular Investor to submit a review."
313
- });
314
- }
315
-
316
- // 2. Get username if not anonymous (use effective CID)
317
- let reviewerUsername = null;
318
- if (!isAnonymous) {
319
- reviewerUsername = await getUsername(db, effectiveCid, config);
320
- }
321
-
322
- // 3. Check if review already exists (for update) - use effective CID for review ID
323
- const reviewId = `${effectiveCid}_${piCid}`;
324
- const existingReview = await db.collection(reviewsCollection).doc(reviewId).get();
325
- const isUpdate = existingReview.exists;
326
-
327
- // 4. Store/Update Review (store effective CID, but track actual CID for audit)
328
- const reviewData = {
329
- userCid: Number(effectiveCid), // Store effective CID (impersonated or actual)
330
- actualUserCid: Number(userCid), // Track actual developer CID for audit
331
- piCid: Number(piCid),
332
- rating: Math.max(1, Math.min(5, ratingNum)),
333
- comment: (comment || "").trim(),
334
- isAnonymous: !!isAnonymous,
335
- reviewerUsername: reviewerUsername,
336
- isImpersonating: isImpersonating || false,
337
- updatedAt: FieldValue.serverTimestamp()
338
- };
339
-
340
- if (!isUpdate) {
341
- reviewData.createdAt = FieldValue.serverTimestamp();
342
- }
343
-
344
- await db.collection(reviewsCollection).doc(reviewId).set(reviewData, { merge: true });
345
-
346
- // Update global rootdata collection for computation system
347
- const { updateRatingsRootData } = require('../rootdata/rootdata_aggregation_helpers');
348
- const today = new Date().toISOString().split('T')[0];
349
- await updateRatingsRootData(db, logger, piCid, effectiveCid, ratingNum, today);
350
-
351
- logger.log('INFO', `[Review] User ${userCid} ${isUpdate ? 'updated' : 'submitted'} review for PI ${piCid}`);
352
- return res.status(200).json({
353
- success: true,
354
- message: isUpdate ? "Review updated." : "Review submitted.",
355
- reviewId
356
- });
357
-
358
- } catch (error) {
359
- logger.log('ERROR', '[Review] Submit failed', error);
360
- return res.status(500).json({ error: "Internal Server Error" });
361
- }
362
- }
363
-
364
- /**
365
- * GET /reviews/{piCid}
366
- * Get all reviews for a Popular Investor with stats
367
- */
368
- async function getReviews(req, res, dependencies, config) {
369
- const { db, logger } = dependencies;
370
- const { piCid } = req.params;
371
- const { reviewsCollection } = config;
372
- const { userCid } = req.query; // Optional: to check if current user has reviewed
373
-
374
- try {
375
- const piCidNum = Number(piCid);
376
-
377
- // Query reviews for this PI
378
- const snapshot = await db.collection(reviewsCollection)
379
- .where('piCid', '==', piCidNum)
380
- .orderBy('createdAt', 'desc')
381
- .limit(100) // Increased limit
382
- .get();
383
-
384
- const reviews = [];
385
- let totalStars = 0;
386
- const ratingDistribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
387
- let currentUserReview = null;
388
-
389
- snapshot.forEach(doc => {
390
- const data = doc.data();
391
- const rating = data.rating || 0;
392
- totalStars += rating;
393
- ratingDistribution[rating] = (ratingDistribution[rating] || 0) + 1;
394
-
395
- const review = {
396
- id: doc.id,
397
- rating: rating,
398
- comment: data.comment || "",
399
- isAnonymous: data.isAnonymous || false,
400
- reviewerUsername: data.isAnonymous ? null : (data.reviewerUsername || null),
401
- userCid: data.userCid || null,
402
- createdAt: data.createdAt ? (data.createdAt.toDate ? data.createdAt.toDate().toISOString() : data.createdAt) : null,
403
- updatedAt: data.updatedAt ? (data.updatedAt.toDate ? data.updatedAt.toDate().toISOString() : data.updatedAt) : null
404
- };
405
-
406
- reviews.push(review);
407
-
408
- // Check if this is the current user's review
409
- if (userCid && Number(data.userCid) === Number(userCid)) {
410
- currentUserReview = review;
411
- }
412
- });
413
-
414
- const count = reviews.length;
415
- const averageRating = count > 0 ? Number((totalStars / count).toFixed(2)) : 0;
416
-
417
- // Calculate percentage distribution
418
- const ratingPercentages = {};
419
- for (let i = 1; i <= 5; i++) {
420
- ratingPercentages[i] = count > 0 ? Number(((ratingDistribution[i] / count) * 100).toFixed(1)) : 0;
421
- }
422
-
423
- return res.status(200).json({
424
- piCid: piCidNum,
425
- count,
426
- averageRating,
427
- ratingDistribution: {
428
- counts: ratingDistribution,
429
- percentages: ratingPercentages
430
- },
431
- reviews,
432
- currentUserReview // Include if userCid was provided
433
- });
434
-
435
- } catch (error) {
436
- logger.log('ERROR', `[getReviews] Error fetching reviews for PI ${piCid}:`, error);
437
- return res.status(500).json({ error: error.message });
438
- }
439
- }
440
-
441
- /**
442
- * GET /me/review/{piCid}
443
- * Get current user's review for a specific PI (if exists)
444
- */
445
- async function getUserReview(req, res, dependencies, config) {
446
- const { db, logger } = dependencies;
447
- const { piCid } = req.params;
448
- const { userCid } = req.query;
449
- const { reviewsCollection } = config;
450
-
451
- if (!userCid) {
452
- return res.status(400).json({ error: "Missing userCid" });
453
- }
454
-
455
- try {
456
- const reviewId = `${userCid}_${piCid}`;
457
- const reviewDoc = await db.collection(reviewsCollection).doc(reviewId).get();
458
-
459
- if (!reviewDoc.exists) {
460
- return res.status(200).json({ exists: false, review: null });
461
- }
462
-
463
- const data = reviewDoc.data();
464
- const review = {
465
- id: reviewDoc.id,
466
- rating: data.rating,
467
- comment: data.comment || "",
468
- isAnonymous: data.isAnonymous || false,
469
- reviewerUsername: data.reviewerUsername || null,
470
- createdAt: data.createdAt ? (data.createdAt.toDate ? data.createdAt.toDate().toISOString() : data.createdAt) : null,
471
- updatedAt: data.updatedAt ? (data.updatedAt.toDate ? data.updatedAt.toDate().toISOString() : data.updatedAt) : null
472
- };
473
-
474
- return res.status(200).json({ exists: true, review });
475
-
476
- } catch (error) {
477
- logger.log('ERROR', `[getUserReview] Error fetching user review:`, error);
478
- return res.status(500).json({ error: error.message });
479
- }
480
- }
481
-
482
- /**
483
- * GET /me/review-eligibility/{piCid}
484
- * Check if current user can review a PI
485
- */
486
- async function checkReviewEligibility(req, res, dependencies, config) {
487
- const { db, logger } = dependencies;
488
- const { piCid } = req.params;
489
- const { userCid } = req.query;
490
-
491
- if (!userCid) {
492
- return res.status(400).json({ error: "Missing userCid" });
493
- }
494
-
495
- try {
496
- // Check for dev override impersonation
497
- const { getEffectiveCid, getDevOverride } = require('../dev/dev_helpers');
498
- const effectiveCid = await getEffectiveCid(db, userCid, config, logger);
499
- const devOverride = await getDevOverride(db, userCid, config, logger);
500
- const isImpersonating = devOverride && devOverride.enabled && devOverride.impersonateCid && effectiveCid !== Number(userCid);
501
-
502
- // Block self-reviews (user cannot review themselves)
503
- if (Number(effectiveCid) === Number(piCid)) {
504
- return res.status(200).json({
505
- piCid: Number(piCid),
506
- eligible: false,
507
- message: "You cannot review your own profile.",
508
- reason: 'self_review'
509
- });
510
- }
511
-
512
- // Check if the effective user is a Popular Investor
513
- const { checkIfUserIsPI } = require('../data_helpers');
514
- const rankEntry = await checkIfUserIsPI(db, effectiveCid, config, logger);
515
-
516
- // Block Popular Investors from reviewing other Popular Investors
517
- // (PIs cannot copy other PIs on the brokerage)
518
- if (rankEntry) {
519
- const targetRankEntry = await checkIfUserIsPI(db, Number(piCid), config, logger);
520
- if (targetRankEntry) {
521
- return res.status(200).json({
522
- piCid: Number(piCid),
523
- eligible: false,
524
- message: "Popular Investors cannot review other Popular Investors.",
525
- reason: 'pi_to_pi_review'
526
- });
527
- }
528
- }
529
-
530
- const canReview = await hasUserCopied(db, effectiveCid, piCid, config);
531
-
532
- logger.log('INFO', `[checkReviewEligibility] User ${effectiveCid} eligibility for PI ${piCid}: ${canReview}`);
533
-
534
- return res.status(200).json({
535
- piCid: Number(piCid),
536
- eligible: canReview,
537
- message: canReview
538
- ? "You are eligible to review this Popular Investor."
539
- : "You must have copied this Popular Investor to submit a review.",
540
- effectiveCid: effectiveCid,
541
- isImpersonating: isImpersonating || false
542
- });
543
-
544
- } catch (error) {
545
- logger.log('ERROR', `[checkReviewEligibility] Error checking eligibility:`, error);
546
- return res.status(500).json({ error: error.message });
547
- }
548
- }
549
-
550
- module.exports = { submitReview, getReviews, getUserReview, checkReviewEligibility };