1use chrono::{DateTime, Utc};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use sqlx::{PgPool, Row};
10use uuid::Uuid;
11
12use crate::error::ReputationError;
13use crate::tier::ReputationTier;
14
15pub struct AnalyticsService {
17 pool: PgPool,
18}
19
20impl AnalyticsService {
21 pub fn new(pool: PgPool) -> Self {
22 Self { pool }
23 }
24
25 pub async fn get_system_stats(&self) -> Result<SystemStats, ReputationError> {
27 let total_users = self.count_total_users().await?;
28 let active_users_30d = self.count_active_users(30).await?;
29 let tier_distribution = self.get_tier_distribution().await?;
30 let avg_score = self.calculate_average_score().await?;
31 let total_commitments = self.count_total_commitments().await?;
32 let completed_commitments = self.count_completed_commitments().await?;
33
34 let completion_rate = if total_commitments > 0 {
35 (completed_commitments as f64 / total_commitments as f64) * 100.0
36 } else {
37 0.0
38 };
39
40 Ok(SystemStats {
41 total_users,
42 active_users_30d,
43 tier_distribution,
44 average_score: avg_score,
45 total_commitments,
46 completed_commitments,
47 completion_rate,
48 generated_at: Utc::now(),
49 })
50 }
51
52 pub async fn get_growth_metrics(&self, days: i32) -> Result<GrowthMetrics, ReputationError> {
54 let start_date = Utc::now() - chrono::Duration::days(days as i64);
55
56 let new_users = self.count_users_since(start_date).await?;
57 let new_commitments = self.count_commitments_since(start_date).await?;
58 let avg_daily_new_users = new_users as f64 / days as f64;
59 let avg_daily_commitments = new_commitments as f64 / days as f64;
60
61 Ok(GrowthMetrics {
62 period_days: days,
63 new_users,
64 new_commitments,
65 avg_daily_new_users,
66 avg_daily_commitments,
67 })
68 }
69
70 pub async fn get_top_performers(
72 &self,
73 limit: i32,
74 ) -> Result<Vec<UserRanking>, ReputationError> {
75 let query = r#"
76 SELECT user_id, reputation_score
77 FROM users
78 WHERE reputation_score IS NOT NULL
79 ORDER BY reputation_score DESC
80 LIMIT $1
81 "#;
82
83 let rows = sqlx::query(query)
84 .bind(limit)
85 .fetch_all(&self.pool)
86 .await
87 .map_err(ReputationError::Database)?;
88
89 let mut rankings = Vec::new();
90 for (rank, row) in rows.iter().enumerate() {
91 let user_id: Uuid = row.try_get("user_id").map_err(ReputationError::Database)?;
92 let score: Decimal = row
93 .try_get("reputation_score")
94 .map_err(ReputationError::Database)?;
95
96 rankings.push(UserRanking {
97 rank: (rank + 1) as i32,
98 user_id,
99 score,
100 tier: ReputationTier::from_score(score),
101 });
102 }
103
104 Ok(rankings)
105 }
106
107 pub async fn get_user_commitment_analytics(
109 &self,
110 user_id: Uuid,
111 ) -> Result<UserCommitmentAnalytics, ReputationError> {
112 let query = r#"
113 SELECT
114 COUNT(*) as total,
115 COUNT(CASE WHEN status = 'Verified' THEN 1 END) as verified,
116 COUNT(CASE WHEN status = 'Failed' THEN 1 END) as failed,
117 COUNT(CASE WHEN status = 'Pending' THEN 1 END) as pending,
118 COUNT(CASE WHEN status = 'Expired' THEN 1 END) as expired
119 FROM output_commitments
120 WHERE user_id = $1
121 "#;
122
123 let row = sqlx::query(query)
124 .bind(user_id)
125 .fetch_one(&self.pool)
126 .await
127 .map_err(ReputationError::Database)?;
128
129 let total: i64 = row.try_get("total").map_err(ReputationError::Database)?;
130 let verified: i64 = row.try_get("verified").map_err(ReputationError::Database)?;
131 let failed: i64 = row.try_get("failed").map_err(ReputationError::Database)?;
132 let pending: i64 = row.try_get("pending").map_err(ReputationError::Database)?;
133 let expired: i64 = row.try_get("expired").map_err(ReputationError::Database)?;
134
135 let success_rate = if total > 0 {
136 (verified as f64 / total as f64) * 100.0
137 } else {
138 0.0
139 };
140
141 Ok(UserCommitmentAnalytics {
142 user_id,
143 total_commitments: total as i32,
144 verified: verified as i32,
145 failed: failed as i32,
146 pending: pending as i32,
147 expired: expired as i32,
148 success_rate,
149 })
150 }
151
152 pub async fn get_score_distribution(&self) -> Result<ScoreDistribution, ReputationError> {
154 let query = r#"
155 SELECT
156 COUNT(CASE WHEN reputation_score < 200 THEN 1 END) as range_0_199,
157 COUNT(CASE WHEN reputation_score >= 200 AND reputation_score < 400 THEN 1 END) as range_200_399,
158 COUNT(CASE WHEN reputation_score >= 400 AND reputation_score < 600 THEN 1 END) as range_400_599,
159 COUNT(CASE WHEN reputation_score >= 600 AND reputation_score < 800 THEN 1 END) as range_600_799,
160 COUNT(CASE WHEN reputation_score >= 800 THEN 1 END) as range_800_plus
161 FROM users
162 WHERE reputation_score IS NOT NULL
163 "#;
164
165 let row = sqlx::query(query)
166 .fetch_one(&self.pool)
167 .await
168 .map_err(ReputationError::Database)?;
169
170 Ok(ScoreDistribution {
171 range_0_199: row
172 .try_get("range_0_199")
173 .map_err(ReputationError::Database)?,
174 range_200_399: row
175 .try_get("range_200_399")
176 .map_err(ReputationError::Database)?,
177 range_400_599: row
178 .try_get("range_400_599")
179 .map_err(ReputationError::Database)?,
180 range_600_799: row
181 .try_get("range_600_799")
182 .map_err(ReputationError::Database)?,
183 range_800_plus: row
184 .try_get("range_800_plus")
185 .map_err(ReputationError::Database)?,
186 })
187 }
188
189 async fn count_total_users(&self) -> Result<i32, ReputationError> {
192 let query = "SELECT COUNT(*) as count FROM users";
193 let row = sqlx::query(query)
194 .fetch_one(&self.pool)
195 .await
196 .map_err(ReputationError::Database)?;
197
198 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
199 Ok(count as i32)
200 }
201
202 async fn count_active_users(&self, days: i32) -> Result<i32, ReputationError> {
203 let since = Utc::now() - chrono::Duration::days(days as i64);
204 let query = r#"
205 SELECT COUNT(DISTINCT user_id) as count
206 FROM reputation_events
207 WHERE created_at >= $1
208 "#;
209
210 let row = sqlx::query(query)
211 .bind(since)
212 .fetch_one(&self.pool)
213 .await
214 .map_err(ReputationError::Database)?;
215
216 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
217 Ok(count as i32)
218 }
219
220 async fn get_tier_distribution(&self) -> Result<Vec<TierCount>, ReputationError> {
221 let query = r#"
222 SELECT
223 CASE
224 WHEN reputation_score < 200 THEN 'Unverified'
225 WHEN reputation_score >= 200 AND reputation_score < 400 THEN 'Bronze'
226 WHEN reputation_score >= 400 AND reputation_score < 600 THEN 'Silver'
227 WHEN reputation_score >= 600 AND reputation_score < 800 THEN 'Gold'
228 WHEN reputation_score >= 800 AND reputation_score < 950 THEN 'Platinum'
229 ELSE 'Diamond'
230 END as tier,
231 COUNT(*) as count
232 FROM users
233 WHERE reputation_score IS NOT NULL
234 GROUP BY tier
235 ORDER BY MIN(reputation_score)
236 "#;
237
238 let rows = sqlx::query(query)
239 .fetch_all(&self.pool)
240 .await
241 .map_err(ReputationError::Database)?;
242
243 let mut distribution = Vec::new();
244 for row in rows {
245 let tier_str: String = row.try_get("tier").map_err(ReputationError::Database)?;
246 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
247
248 let tier = tier_str
249 .parse::<ReputationTier>()
250 .map_err(|e| ReputationError::Validation(e.to_string()))?;
251
252 distribution.push(TierCount {
253 tier,
254 count: count as i32,
255 });
256 }
257
258 Ok(distribution)
259 }
260
261 async fn calculate_average_score(&self) -> Result<Decimal, ReputationError> {
262 let query =
263 "SELECT AVG(reputation_score) as avg FROM users WHERE reputation_score IS NOT NULL";
264 let row = sqlx::query(query)
265 .fetch_one(&self.pool)
266 .await
267 .map_err(ReputationError::Database)?;
268
269 let avg: Option<Decimal> = row.try_get("avg").map_err(ReputationError::Database)?;
270 Ok(avg.unwrap_or(Decimal::ZERO))
271 }
272
273 async fn count_total_commitments(&self) -> Result<i32, ReputationError> {
274 let query = "SELECT COUNT(*) as count FROM output_commitments";
275 let row = sqlx::query(query)
276 .fetch_one(&self.pool)
277 .await
278 .map_err(ReputationError::Database)?;
279
280 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
281 Ok(count as i32)
282 }
283
284 async fn count_completed_commitments(&self) -> Result<i32, ReputationError> {
285 let query = "SELECT COUNT(*) as count FROM output_commitments WHERE status = 'Verified'";
286 let row = sqlx::query(query)
287 .fetch_one(&self.pool)
288 .await
289 .map_err(ReputationError::Database)?;
290
291 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
292 Ok(count as i32)
293 }
294
295 async fn count_users_since(&self, since: DateTime<Utc>) -> Result<i32, ReputationError> {
296 let query = "SELECT COUNT(*) as count FROM users WHERE created_at >= $1";
297 let row = sqlx::query(query)
298 .bind(since)
299 .fetch_one(&self.pool)
300 .await
301 .map_err(ReputationError::Database)?;
302
303 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
304 Ok(count as i32)
305 }
306
307 async fn count_commitments_since(&self, since: DateTime<Utc>) -> Result<i32, ReputationError> {
308 let query = "SELECT COUNT(*) as count FROM output_commitments WHERE created_at >= $1";
309 let row = sqlx::query(query)
310 .bind(since)
311 .fetch_one(&self.pool)
312 .await
313 .map_err(ReputationError::Database)?;
314
315 let count: i64 = row.try_get("count").map_err(ReputationError::Database)?;
316 Ok(count as i32)
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct SystemStats {
323 pub total_users: i32,
324 pub active_users_30d: i32,
325 pub tier_distribution: Vec<TierCount>,
326 pub average_score: Decimal,
327 pub total_commitments: i32,
328 pub completed_commitments: i32,
329 pub completion_rate: f64,
330 pub generated_at: DateTime<Utc>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct TierCount {
336 pub tier: ReputationTier,
337 pub count: i32,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct GrowthMetrics {
343 pub period_days: i32,
344 pub new_users: i32,
345 pub new_commitments: i32,
346 pub avg_daily_new_users: f64,
347 pub avg_daily_commitments: f64,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct UserRanking {
353 pub rank: i32,
354 pub user_id: Uuid,
355 pub score: Decimal,
356 pub tier: ReputationTier,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct UserCommitmentAnalytics {
362 pub user_id: Uuid,
363 pub total_commitments: i32,
364 pub verified: i32,
365 pub failed: i32,
366 pub pending: i32,
367 pub expired: i32,
368 pub success_rate: f64,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct ScoreDistribution {
374 pub range_0_199: i64,
375 pub range_200_399: i64,
376 pub range_400_599: i64,
377 pub range_600_799: i64,
378 pub range_800_plus: i64,
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_system_stats_structure() {
387 let stats = SystemStats {
388 total_users: 1000,
389 active_users_30d: 750,
390 tier_distribution: vec![TierCount {
391 tier: ReputationTier::Gold,
392 count: 100,
393 }],
394 average_score: Decimal::new(650, 0),
395 total_commitments: 5000,
396 completed_commitments: 4000,
397 completion_rate: 80.0,
398 generated_at: Utc::now(),
399 };
400
401 assert_eq!(stats.total_users, 1000);
402 assert_eq!(stats.completion_rate, 80.0);
403 }
404
405 #[test]
406 fn test_growth_metrics_calculation() {
407 let metrics = GrowthMetrics {
408 period_days: 30,
409 new_users: 300,
410 new_commitments: 1500,
411 avg_daily_new_users: 10.0,
412 avg_daily_commitments: 50.0,
413 };
414
415 assert_eq!(metrics.avg_daily_new_users, 10.0);
416 assert_eq!(metrics.avg_daily_commitments, 50.0);
417 }
418
419 #[test]
420 fn test_user_ranking_structure() {
421 let ranking = UserRanking {
422 rank: 1,
423 user_id: Uuid::new_v4(),
424 score: Decimal::new(950, 0),
425 tier: ReputationTier::Diamond,
426 };
427
428 assert_eq!(ranking.rank, 1);
429 assert_eq!(ranking.tier, ReputationTier::Diamond);
430 }
431
432 #[test]
433 fn test_commitment_analytics_success_rate() {
434 let analytics = UserCommitmentAnalytics {
435 user_id: Uuid::new_v4(),
436 total_commitments: 100,
437 verified: 80,
438 failed: 10,
439 pending: 5,
440 expired: 5,
441 success_rate: 80.0,
442 };
443
444 assert_eq!(analytics.success_rate, 80.0);
445 assert_eq!(
446 analytics.verified + analytics.failed + analytics.pending + analytics.expired,
447 analytics.total_commitments
448 );
449 }
450
451 #[test]
452 fn test_score_distribution_structure() {
453 let dist = ScoreDistribution {
454 range_0_199: 100,
455 range_200_399: 200,
456 range_400_599: 300,
457 range_600_799: 250,
458 range_800_plus: 150,
459 };
460
461 let total = dist.range_0_199
462 + dist.range_200_399
463 + dist.range_400_599
464 + dist.range_600_799
465 + dist.range_800_plus;
466 assert_eq!(total, 1000);
467 }
468}