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.
- package/functions/alert-system/helpers/alert_helpers.js +6 -6
- package/functions/alert-system/index.js +1 -1
- package/functions/api-v2/helpers/data-fetchers/firestore.js +2218 -0
- package/functions/api-v2/helpers/task_engine_helper.js +51 -0
- package/functions/api-v2/index.js +36 -0
- package/functions/api-v2/middleware/identity_middleware.js +48 -0
- package/functions/api-v2/package.json +6 -0
- package/functions/api-v2/routes/alerts.js +168 -0
- package/functions/api-v2/routes/index.js +35 -0
- package/functions/api-v2/routes/notifications.js +38 -0
- package/functions/api-v2/routes/popular_investors.js +204 -0
- package/functions/api-v2/routes/profile.js +212 -0
- package/functions/api-v2/routes/reviews.js +72 -0
- package/functions/api-v2/routes/settings.js +71 -0
- package/functions/api-v2/routes/sync.js +132 -0
- package/functions/api-v2/routes/verification.js +47 -0
- package/functions/api-v2/routes/watchlists.js +148 -0
- package/functions/computation-system/helpers/computation_worker.js +1 -1
- package/functions/task-engine/helpers/popular_investor_helpers.js +2 -2
- package/index.js +6 -2
- package/package.json +2 -1
- package/functions/generic-api/admin-api/index.js +0 -895
- package/functions/generic-api/helpers/api_helpers.js +0 -457
- package/functions/generic-api/index.js +0 -204
- package/functions/generic-api/user-api/ADDING_LEGACY_ROUTES_GUIDE.md +0 -345
- package/functions/generic-api/user-api/CODE_REORGANIZATION_PLAN.md +0 -320
- package/functions/generic-api/user-api/COMPLETE_REFACTORING_PLAN.md +0 -116
- package/functions/generic-api/user-api/FIRESTORE_PATHS_INVENTORY.md +0 -171
- package/functions/generic-api/user-api/FIRESTORE_PATH_MIGRATION_REFERENCE.md +0 -710
- package/functions/generic-api/user-api/FIRESTORE_PATH_VALIDATION.md +0 -109
- package/functions/generic-api/user-api/MIGRATION_PLAN.md +0 -499
- package/functions/generic-api/user-api/README_MIGRATION.md +0 -152
- package/functions/generic-api/user-api/REFACTORING_COMPLETE.md +0 -106
- package/functions/generic-api/user-api/REFACTORING_STATUS.md +0 -85
- package/functions/generic-api/user-api/VERIFICATION_MIGRATION_NOTES.md +0 -206
- package/functions/generic-api/user-api/helpers/ORGANIZATION_COMPLETE.md +0 -126
- package/functions/generic-api/user-api/helpers/alerts/alert_helpers.js +0 -355
- package/functions/generic-api/user-api/helpers/alerts/subscription_helpers.js +0 -327
- package/functions/generic-api/user-api/helpers/alerts/test_alert_helpers.js +0 -212
- package/functions/generic-api/user-api/helpers/collection_helpers.js +0 -193
- package/functions/generic-api/user-api/helpers/core/compression_helpers.js +0 -68
- package/functions/generic-api/user-api/helpers/core/data_lookup_helpers.js +0 -256
- package/functions/generic-api/user-api/helpers/core/path_resolution_helpers.js +0 -640
- package/functions/generic-api/user-api/helpers/core/user_status_helpers.js +0 -195
- package/functions/generic-api/user-api/helpers/data/computation_helpers.js +0 -503
- package/functions/generic-api/user-api/helpers/data/instrument_helpers.js +0 -55
- package/functions/generic-api/user-api/helpers/data/portfolio_helpers.js +0 -245
- package/functions/generic-api/user-api/helpers/data/social_helpers.js +0 -174
- package/functions/generic-api/user-api/helpers/data_helpers.js +0 -87
- package/functions/generic-api/user-api/helpers/dev/dev_helpers.js +0 -336
- package/functions/generic-api/user-api/helpers/fetch/on_demand_fetch_helpers.js +0 -615
- package/functions/generic-api/user-api/helpers/metrics/personalized_metrics_helpers.js +0 -231
- package/functions/generic-api/user-api/helpers/notifications/notification_helpers.js +0 -641
- package/functions/generic-api/user-api/helpers/profile/pi_profile_helpers.js +0 -182
- package/functions/generic-api/user-api/helpers/profile/profile_view_helpers.js +0 -137
- package/functions/generic-api/user-api/helpers/profile/user_profile_helpers.js +0 -190
- package/functions/generic-api/user-api/helpers/recommendations/recommendation_helpers.js +0 -66
- package/functions/generic-api/user-api/helpers/reviews/review_helpers.js +0 -550
- package/functions/generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers.js +0 -378
- package/functions/generic-api/user-api/helpers/search/pi_request_helpers.js +0 -295
- package/functions/generic-api/user-api/helpers/search/pi_search_helpers.js +0 -162
- package/functions/generic-api/user-api/helpers/sync/user_sync_helpers.js +0 -677
- package/functions/generic-api/user-api/helpers/verification/verification_helpers.js +0 -323
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_analytics_helpers.js +0 -96
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_data_helpers.js +0 -141
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_generation_helpers.js +0 -310
- package/functions/generic-api/user-api/helpers/watchlist/watchlist_management_helpers.js +0 -829
- 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 };
|