bulltrackers-module 1.0.733 → 1.0.734
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/computation-system-v2/README.md +152 -0
- package/functions/computation-system-v2/computations/PopularInvestorProfileMetrics.js +720 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskAssessment.js +176 -0
- package/functions/computation-system-v2/computations/PopularInvestorRiskMetrics.js +294 -0
- package/functions/computation-system-v2/computations/TestComputation.js +46 -0
- package/functions/computation-system-v2/computations/UserPortfolioSummary.js +172 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +317 -0
- package/functions/computation-system-v2/framework/core/Computation.js +73 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +223 -0
- package/functions/computation-system-v2/framework/core/RuleInjector.js +53 -0
- package/functions/computation-system-v2/framework/core/Rules.js +231 -0
- package/functions/computation-system-v2/framework/core/RunAnalyzer.js +163 -0
- package/functions/computation-system-v2/framework/cost/CostTracker.js +154 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +399 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +232 -0
- package/functions/computation-system-v2/framework/data/SchemaRegistry.js +287 -0
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +498 -0
- package/functions/computation-system-v2/framework/execution/TaskRunner.js +35 -0
- package/functions/computation-system-v2/framework/execution/middleware/CostTrackerMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/LineageMiddleware.js +32 -0
- package/functions/computation-system-v2/framework/execution/middleware/Middleware.js +14 -0
- package/functions/computation-system-v2/framework/execution/middleware/ProfilerMiddleware.js +47 -0
- package/functions/computation-system-v2/framework/index.js +45 -0
- package/functions/computation-system-v2/framework/lineage/LineageTracker.js +147 -0
- package/functions/computation-system-v2/framework/monitoring/Profiler.js +80 -0
- package/functions/computation-system-v2/framework/resilience/Checkpointer.js +66 -0
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +327 -0
- package/functions/computation-system-v2/framework/storage/StateRepository.js +286 -0
- package/functions/computation-system-v2/framework/storage/StorageManager.js +469 -0
- package/functions/computation-system-v2/framework/storage/index.js +9 -0
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +86 -0
- package/functions/computation-system-v2/framework/utils/Graph.js +205 -0
- package/functions/computation-system-v2/handlers/dispatcher.js +109 -0
- package/functions/computation-system-v2/handlers/index.js +23 -0
- package/functions/computation-system-v2/handlers/onDemand.js +289 -0
- package/functions/computation-system-v2/handlers/scheduler.js +327 -0
- package/functions/computation-system-v2/index.js +163 -0
- package/functions/computation-system-v2/rules/index.js +49 -0
- package/functions/computation-system-v2/rules/instruments.js +465 -0
- package/functions/computation-system-v2/rules/metrics.js +304 -0
- package/functions/computation-system-v2/rules/portfolio.js +534 -0
- package/functions/computation-system-v2/rules/rankings.js +655 -0
- package/functions/computation-system-v2/rules/social.js +562 -0
- package/functions/computation-system-v2/rules/trades.js +545 -0
- package/functions/computation-system-v2/scripts/migrate-sectors.js +73 -0
- package/functions/computation-system-v2/test/test-dispatcher.js +317 -0
- package/functions/computation-system-v2/test/test-framework.js +500 -0
- package/functions/computation-system-v2/test/test-real-execution.js +166 -0
- package/functions/computation-system-v2/test/test-real-integration.js +194 -0
- package/functions/computation-system-v2/test/test-refactor-e2e.js +131 -0
- package/functions/computation-system-v2/test/test-results.json +31 -0
- package/functions/computation-system-v2/test/test-risk-metrics-computation.js +329 -0
- package/functions/computation-system-v2/test/test-scheduler.js +204 -0
- package/functions/computation-system-v2/test/test-storage.js +449 -0
- package/functions/orchestrator/index.js +18 -26
- package/package.json +3 -2
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Social & Engagement Business Rules
|
|
3
|
+
*
|
|
4
|
+
* For extracting and analyzing social data:
|
|
5
|
+
* - social_post_snapshots (posts, engagement)
|
|
6
|
+
* - pi_ratings (reviews, ratings)
|
|
7
|
+
* - pi_page_views (profile views)
|
|
8
|
+
*
|
|
9
|
+
* Usage in computation:
|
|
10
|
+
* ```javascript
|
|
11
|
+
* const posts = rules.social.extractPosts(row);
|
|
12
|
+
* const engagement = rules.social.calculateEngagement(posts);
|
|
13
|
+
* const rating = rules.social.getAverageRating(ratingsRow);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// POSTS - social_post_snapshots
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract posts from a social_post_snapshots row.
|
|
23
|
+
* @param {Object} row - social_post_snapshots row
|
|
24
|
+
* @returns {Array} Array of post objects
|
|
25
|
+
*/
|
|
26
|
+
function extractPosts(row) {
|
|
27
|
+
if (!row) return [];
|
|
28
|
+
|
|
29
|
+
let data = row.posts_data;
|
|
30
|
+
|
|
31
|
+
if (typeof data === 'string') {
|
|
32
|
+
try {
|
|
33
|
+
data = JSON.parse(data);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Posts are in a map keyed by post ID
|
|
40
|
+
const postsMap = data?.posts;
|
|
41
|
+
if (!postsMap || typeof postsMap !== 'object') return [];
|
|
42
|
+
|
|
43
|
+
return Object.values(postsMap);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get user ID from posts row.
|
|
48
|
+
* @param {Object} row - social_post_snapshots row
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
function getPostsUserId(row) {
|
|
52
|
+
if (!row) return null;
|
|
53
|
+
|
|
54
|
+
if (row.user_id) return String(row.user_id);
|
|
55
|
+
|
|
56
|
+
let data = row.posts_data;
|
|
57
|
+
if (typeof data === 'string') {
|
|
58
|
+
try {
|
|
59
|
+
data = JSON.parse(data);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return data?.cid ? String(data.cid) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get post count from row.
|
|
70
|
+
* @param {Object} row - social_post_snapshots row
|
|
71
|
+
* @returns {number}
|
|
72
|
+
*/
|
|
73
|
+
function getPostCount(row) {
|
|
74
|
+
if (!row) return 0;
|
|
75
|
+
|
|
76
|
+
let data = row.posts_data;
|
|
77
|
+
if (typeof data === 'string') {
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.parse(data);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return data?.postCount ?? 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get post text.
|
|
90
|
+
* @param {Object} post - Post object
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
function getPostText(post) {
|
|
94
|
+
return post?.text || '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get post creation date.
|
|
99
|
+
* @param {Object} post - Post object
|
|
100
|
+
* @returns {Date|null}
|
|
101
|
+
*/
|
|
102
|
+
function getPostDate(post) {
|
|
103
|
+
return post?.createdAt ? new Date(post.createdAt) : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get post likes count.
|
|
108
|
+
* @param {Object} post - Post object
|
|
109
|
+
* @returns {number}
|
|
110
|
+
*/
|
|
111
|
+
function getPostLikes(post) {
|
|
112
|
+
return post?.stats?.likes ?? 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get post comments count.
|
|
117
|
+
* @param {Object} post - Post object
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
function getPostComments(post) {
|
|
121
|
+
return post?.stats?.comments ?? 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get total engagement (likes + comments).
|
|
126
|
+
* @param {Object} post - Post object
|
|
127
|
+
* @returns {number}
|
|
128
|
+
*/
|
|
129
|
+
function getPostEngagement(post) {
|
|
130
|
+
return getPostLikes(post) + getPostComments(post);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get post ID.
|
|
135
|
+
* @param {Object} post - Post object
|
|
136
|
+
* @returns {string|null}
|
|
137
|
+
*/
|
|
138
|
+
function getPostId(post) {
|
|
139
|
+
return post?.id || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract mentioned tickers from post text.
|
|
144
|
+
* Looks for $TICKER patterns.
|
|
145
|
+
* @param {Object} post - Post object
|
|
146
|
+
* @returns {string[]}
|
|
147
|
+
*/
|
|
148
|
+
function extractMentionedTickers(post) {
|
|
149
|
+
const text = getPostText(post);
|
|
150
|
+
const matches = text.match(/\$([A-Z0-9.]+)/g) || [];
|
|
151
|
+
return [...new Set(matches.map(m => m.substring(1)))];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Calculate total engagement for all posts.
|
|
156
|
+
* @param {Array} posts - Array of posts
|
|
157
|
+
* @returns {Object}
|
|
158
|
+
*/
|
|
159
|
+
function calculateEngagement(posts) {
|
|
160
|
+
if (!posts || posts.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
totalPosts: 0,
|
|
163
|
+
totalLikes: 0,
|
|
164
|
+
totalComments: 0,
|
|
165
|
+
totalEngagement: 0,
|
|
166
|
+
avgLikesPerPost: 0,
|
|
167
|
+
avgCommentsPerPost: 0,
|
|
168
|
+
avgEngagementPerPost: 0
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const totalLikes = posts.reduce((sum, p) => sum + getPostLikes(p), 0);
|
|
173
|
+
const totalComments = posts.reduce((sum, p) => sum + getPostComments(p), 0);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
totalPosts: posts.length,
|
|
177
|
+
totalLikes,
|
|
178
|
+
totalComments,
|
|
179
|
+
totalEngagement: totalLikes + totalComments,
|
|
180
|
+
avgLikesPerPost: totalLikes / posts.length,
|
|
181
|
+
avgCommentsPerPost: totalComments / posts.length,
|
|
182
|
+
avgEngagementPerPost: (totalLikes + totalComments) / posts.length
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get most engaged posts.
|
|
188
|
+
* @param {Array} posts - Array of posts
|
|
189
|
+
* @param {number} count - Number to return
|
|
190
|
+
* @returns {Array}
|
|
191
|
+
*/
|
|
192
|
+
function getMostEngagedPosts(posts, count = 5) {
|
|
193
|
+
return [...posts]
|
|
194
|
+
.sort((a, b) => getPostEngagement(b) - getPostEngagement(a))
|
|
195
|
+
.slice(0, count);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get recent posts.
|
|
200
|
+
* @param {Array} posts - Array of posts
|
|
201
|
+
* @param {number} count - Number to return
|
|
202
|
+
* @returns {Array}
|
|
203
|
+
*/
|
|
204
|
+
function getRecentPosts(posts, count = 5) {
|
|
205
|
+
return [...posts]
|
|
206
|
+
.sort((a, b) => {
|
|
207
|
+
const dateA = getPostDate(a);
|
|
208
|
+
const dateB = getPostDate(b);
|
|
209
|
+
return (dateB || 0) - (dateA || 0);
|
|
210
|
+
})
|
|
211
|
+
.slice(0, count);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Calculate posting frequency (posts per week).
|
|
216
|
+
* @param {Array} posts - Array of posts
|
|
217
|
+
* @returns {number}
|
|
218
|
+
*/
|
|
219
|
+
function calculatePostFrequency(posts) {
|
|
220
|
+
if (!posts || posts.length < 2) return 0;
|
|
221
|
+
|
|
222
|
+
const dates = posts
|
|
223
|
+
.map(getPostDate)
|
|
224
|
+
.filter(d => d)
|
|
225
|
+
.sort((a, b) => a - b);
|
|
226
|
+
|
|
227
|
+
if (dates.length < 2) return 0;
|
|
228
|
+
|
|
229
|
+
const daySpan = (dates[dates.length - 1] - dates[0]) / (1000 * 60 * 60 * 24);
|
|
230
|
+
if (daySpan === 0) return posts.length; // All same day
|
|
231
|
+
|
|
232
|
+
return (posts.length / daySpan) * 7;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// RATINGS - pi_ratings
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get average rating from pi_ratings row.
|
|
241
|
+
* @param {Object} row - pi_ratings row
|
|
242
|
+
* @returns {number}
|
|
243
|
+
*/
|
|
244
|
+
function getAverageRating(row) {
|
|
245
|
+
return row?.average_rating ?? 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get total ratings count.
|
|
250
|
+
* @param {Object} row - pi_ratings row
|
|
251
|
+
* @returns {number}
|
|
252
|
+
*/
|
|
253
|
+
function getTotalRatings(row) {
|
|
254
|
+
return row?.total_ratings ?? 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract reviews from pi_ratings row.
|
|
259
|
+
* @param {Object} row - pi_ratings row
|
|
260
|
+
* @returns {Array}
|
|
261
|
+
*/
|
|
262
|
+
function extractReviews(row) {
|
|
263
|
+
if (!row) return [];
|
|
264
|
+
|
|
265
|
+
let reviews = row.reviews;
|
|
266
|
+
|
|
267
|
+
if (typeof reviews === 'string') {
|
|
268
|
+
try {
|
|
269
|
+
reviews = JSON.parse(reviews);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Array.isArray(reviews) ? reviews : [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get ratings by user map.
|
|
280
|
+
* @param {Object} row - pi_ratings row
|
|
281
|
+
* @returns {Object} Map of userId -> rating
|
|
282
|
+
*/
|
|
283
|
+
function getRatingsByUser(row) {
|
|
284
|
+
if (!row) return {};
|
|
285
|
+
|
|
286
|
+
let ratings = row.ratings_by_user;
|
|
287
|
+
|
|
288
|
+
if (typeof ratings === 'string') {
|
|
289
|
+
try {
|
|
290
|
+
ratings = JSON.parse(ratings);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return ratings || {};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get review comment.
|
|
301
|
+
* @param {Object} review - Review object
|
|
302
|
+
* @returns {string}
|
|
303
|
+
*/
|
|
304
|
+
function getReviewComment(review) {
|
|
305
|
+
return review?.comment || '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get review rating (1-5).
|
|
310
|
+
* @param {Object} review - Review object
|
|
311
|
+
* @returns {number}
|
|
312
|
+
*/
|
|
313
|
+
function getReviewRating(review) {
|
|
314
|
+
return review?.rating ?? 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get review date.
|
|
319
|
+
* @param {Object} review - Review object
|
|
320
|
+
* @returns {Date|null}
|
|
321
|
+
*/
|
|
322
|
+
function getReviewDate(review) {
|
|
323
|
+
return review?.createdAt ? new Date(review.createdAt) : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get reviewer username.
|
|
328
|
+
* @param {Object} review - Review object
|
|
329
|
+
* @returns {string|null}
|
|
330
|
+
*/
|
|
331
|
+
function getReviewerUsername(review) {
|
|
332
|
+
return review?.reviewerUsername || null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Check if review is anonymous.
|
|
337
|
+
* @param {Object} review - Review object
|
|
338
|
+
* @returns {boolean}
|
|
339
|
+
*/
|
|
340
|
+
function isReviewAnonymous(review) {
|
|
341
|
+
return review?.isAnonymous === true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Calculate rating distribution.
|
|
346
|
+
* @param {Array} reviews - Array of reviews
|
|
347
|
+
* @returns {Object} Map of rating -> count
|
|
348
|
+
*/
|
|
349
|
+
function calculateRatingDistribution(reviews) {
|
|
350
|
+
const distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
351
|
+
|
|
352
|
+
for (const review of reviews) {
|
|
353
|
+
const rating = getReviewRating(review);
|
|
354
|
+
if (rating >= 1 && rating <= 5) {
|
|
355
|
+
distribution[rating]++;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return distribution;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get recent reviews.
|
|
364
|
+
* @param {Array} reviews - Array of reviews
|
|
365
|
+
* @param {number} count - Number to return
|
|
366
|
+
* @returns {Array}
|
|
367
|
+
*/
|
|
368
|
+
function getRecentReviews(reviews, count = 5) {
|
|
369
|
+
return [...reviews]
|
|
370
|
+
.sort((a, b) => {
|
|
371
|
+
const dateA = getReviewDate(a);
|
|
372
|
+
const dateB = getReviewDate(b);
|
|
373
|
+
return (dateB || 0) - (dateA || 0);
|
|
374
|
+
})
|
|
375
|
+
.slice(0, count);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// =============================================================================
|
|
379
|
+
// PAGE VIEWS - pi_page_views
|
|
380
|
+
// =============================================================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Get total views from pi_page_views row.
|
|
384
|
+
* @param {Object} row - pi_page_views row
|
|
385
|
+
* @returns {number}
|
|
386
|
+
*/
|
|
387
|
+
function getTotalViews(row) {
|
|
388
|
+
return row?.total_views ?? 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get unique viewers count.
|
|
393
|
+
* @param {Object} row - pi_page_views row
|
|
394
|
+
* @returns {number}
|
|
395
|
+
*/
|
|
396
|
+
function getUniqueViewers(row) {
|
|
397
|
+
return row?.unique_viewers ?? 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Extract views by user map.
|
|
402
|
+
* @param {Object} row - pi_page_views row
|
|
403
|
+
* @returns {Object} Map of userId -> viewData
|
|
404
|
+
*/
|
|
405
|
+
function extractViewsByUser(row) {
|
|
406
|
+
if (!row) return {};
|
|
407
|
+
|
|
408
|
+
let views = row.views_by_user;
|
|
409
|
+
|
|
410
|
+
if (typeof views === 'string') {
|
|
411
|
+
try {
|
|
412
|
+
views = JSON.parse(views);
|
|
413
|
+
} catch (e) {
|
|
414
|
+
return {};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return views || {};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get view count for a specific user.
|
|
423
|
+
* @param {Object} viewsByUser - views_by_user map
|
|
424
|
+
* @param {string} userId - User ID
|
|
425
|
+
* @returns {number}
|
|
426
|
+
*/
|
|
427
|
+
function getViewCountForUser(viewsByUser, userId) {
|
|
428
|
+
return viewsByUser?.[userId]?.viewCount ?? 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get last viewed timestamp for a specific user.
|
|
433
|
+
* @param {Object} viewsByUser - views_by_user map
|
|
434
|
+
* @param {string} userId - User ID
|
|
435
|
+
* @returns {Date|null}
|
|
436
|
+
*/
|
|
437
|
+
function getLastViewedForUser(viewsByUser, userId) {
|
|
438
|
+
const lastViewed = viewsByUser?.[userId]?.lastViewed;
|
|
439
|
+
if (!lastViewed) return null;
|
|
440
|
+
|
|
441
|
+
// Handle Firestore timestamp format
|
|
442
|
+
if (lastViewed._seconds) {
|
|
443
|
+
return new Date(lastViewed._seconds * 1000 + (lastViewed._nanoseconds || 0) / 1000000);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return new Date(lastViewed);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Calculate view metrics.
|
|
451
|
+
* @param {Object} row - pi_page_views row
|
|
452
|
+
* @returns {Object}
|
|
453
|
+
*/
|
|
454
|
+
function calculateViewMetrics(row) {
|
|
455
|
+
const total = getTotalViews(row);
|
|
456
|
+
const unique = getUniqueViewers(row);
|
|
457
|
+
const viewsByUser = extractViewsByUser(row);
|
|
458
|
+
|
|
459
|
+
const userIds = Object.keys(viewsByUser);
|
|
460
|
+
const viewCounts = userIds.map(id => getViewCountForUser(viewsByUser, id));
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
totalViews: total,
|
|
464
|
+
uniqueViewers: unique,
|
|
465
|
+
avgViewsPerViewer: unique > 0 ? total / unique : 0,
|
|
466
|
+
maxViewsByUser: viewCounts.length > 0 ? Math.max(...viewCounts) : 0,
|
|
467
|
+
repeatViewers: viewCounts.filter(c => c > 1).length
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// =============================================================================
|
|
472
|
+
// COMBINED SOCIAL SCORE
|
|
473
|
+
// =============================================================================
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Calculate a social engagement score (0-100).
|
|
477
|
+
* @param {Object} options - Data sources
|
|
478
|
+
* @param {Object} [options.postsRow] - social_post_snapshots row
|
|
479
|
+
* @param {Object} [options.ratingsRow] - pi_ratings row
|
|
480
|
+
* @param {Object} [options.viewsRow] - pi_page_views row
|
|
481
|
+
* @returns {number}
|
|
482
|
+
*/
|
|
483
|
+
function calculateSocialScore(options = {}) {
|
|
484
|
+
let score = 50; // Base score
|
|
485
|
+
|
|
486
|
+
// Posts engagement (+/- 15)
|
|
487
|
+
if (options.postsRow) {
|
|
488
|
+
const posts = extractPosts(options.postsRow);
|
|
489
|
+
const engagement = calculateEngagement(posts);
|
|
490
|
+
|
|
491
|
+
if (engagement.avgEngagementPerPost >= 5) score += 15;
|
|
492
|
+
else if (engagement.avgEngagementPerPost >= 2) score += 7;
|
|
493
|
+
else if (engagement.totalPosts === 0) score -= 10;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Ratings (+/- 20)
|
|
497
|
+
if (options.ratingsRow) {
|
|
498
|
+
const avgRating = getAverageRating(options.ratingsRow);
|
|
499
|
+
const totalRatings = getTotalRatings(options.ratingsRow);
|
|
500
|
+
|
|
501
|
+
if (avgRating >= 4.5 && totalRatings >= 5) score += 20;
|
|
502
|
+
else if (avgRating >= 4.0 && totalRatings >= 3) score += 10;
|
|
503
|
+
else if (avgRating < 3.0 && totalRatings >= 3) score -= 15;
|
|
504
|
+
else if (totalRatings === 0) score -= 5;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Page views (+/- 15)
|
|
508
|
+
if (options.viewsRow) {
|
|
509
|
+
const metrics = calculateViewMetrics(options.viewsRow);
|
|
510
|
+
|
|
511
|
+
if (metrics.uniqueViewers >= 50) score += 15;
|
|
512
|
+
else if (metrics.uniqueViewers >= 20) score += 7;
|
|
513
|
+
else if (metrics.uniqueViewers < 5) score -= 5;
|
|
514
|
+
|
|
515
|
+
// Repeat viewers bonus
|
|
516
|
+
if (metrics.repeatViewers >= 10) score += 5;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return Math.max(0, Math.min(100, score));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = {
|
|
523
|
+
// Posts
|
|
524
|
+
extractPosts,
|
|
525
|
+
getPostsUserId,
|
|
526
|
+
getPostCount,
|
|
527
|
+
getPostText,
|
|
528
|
+
getPostDate,
|
|
529
|
+
getPostLikes,
|
|
530
|
+
getPostComments,
|
|
531
|
+
getPostEngagement,
|
|
532
|
+
getPostId,
|
|
533
|
+
extractMentionedTickers,
|
|
534
|
+
calculateEngagement,
|
|
535
|
+
getMostEngagedPosts,
|
|
536
|
+
getRecentPosts,
|
|
537
|
+
calculatePostFrequency,
|
|
538
|
+
|
|
539
|
+
// Ratings
|
|
540
|
+
getAverageRating,
|
|
541
|
+
getTotalRatings,
|
|
542
|
+
extractReviews,
|
|
543
|
+
getRatingsByUser,
|
|
544
|
+
getReviewComment,
|
|
545
|
+
getReviewRating,
|
|
546
|
+
getReviewDate,
|
|
547
|
+
getReviewerUsername,
|
|
548
|
+
isReviewAnonymous,
|
|
549
|
+
calculateRatingDistribution,
|
|
550
|
+
getRecentReviews,
|
|
551
|
+
|
|
552
|
+
// Page views
|
|
553
|
+
getTotalViews,
|
|
554
|
+
getUniqueViewers,
|
|
555
|
+
extractViewsByUser,
|
|
556
|
+
getViewCountForUser,
|
|
557
|
+
getLastViewedForUser,
|
|
558
|
+
calculateViewMetrics,
|
|
559
|
+
|
|
560
|
+
// Combined
|
|
561
|
+
calculateSocialScore
|
|
562
|
+
};
|