kaccy_reputation/
analytics.rs

1//! Analytics module for reputation system insights
2//!
3//! Provides comprehensive statistics, trends, and insights across
4//! the entire reputation system for monitoring and decision-making.
5
6use 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
15/// Service for generating analytics and insights
16pub struct AnalyticsService {
17    pool: PgPool,
18}
19
20impl AnalyticsService {
21    pub fn new(pool: PgPool) -> Self {
22        Self { pool }
23    }
24
25    /// Get overall system statistics
26    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    /// Get user growth metrics over time
53    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    /// Get top performers by score
71    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    /// Get commitment analytics for a specific user
108    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    /// Get score distribution across all users
153    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    // Helper methods
190
191    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/// Overall system statistics
321#[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/// User count per tier
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct TierCount {
336    pub tier: ReputationTier,
337    pub count: i32,
338}
339
340/// Growth metrics over a period
341#[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/// User ranking information
351#[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/// User-specific commitment analytics
360#[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/// Score distribution across ranges
372#[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}