koprogo_api/application/use_cases/
gamification_use_cases.rs

1use crate::application::dto::{
2    AchievementResponseDto, ChallengeProgressResponseDto, ChallengeResponseDto,
3    CreateAchievementDto, CreateChallengeDto, LeaderboardEntryDto, LeaderboardResponseDto,
4    UpdateAchievementDto, UpdateChallengeDto, UserAchievementResponseDto, UserGamificationStatsDto,
5};
6use crate::application::ports::{
7    AchievementRepository, ChallengeProgressRepository, ChallengeRepository,
8    UserAchievementRepository, UserRepository,
9};
10use crate::domain::entities::{
11    Achievement, AchievementCategory, Challenge, ChallengeProgress, ChallengeStatus,
12    UserAchievement,
13};
14use std::sync::Arc;
15use uuid::Uuid;
16
17// ============================================================================
18// Achievement Use Cases
19// ============================================================================
20
21/// Use cases for achievement operations
22///
23/// Orchestrates business logic for achievement CRUD, awarding achievements to users,
24/// and calculating user achievement statistics.
25pub struct AchievementUseCases {
26    achievement_repo: Arc<dyn AchievementRepository>,
27    user_achievement_repo: Arc<dyn UserAchievementRepository>,
28    #[allow(dead_code)]
29    user_repo: Arc<dyn UserRepository>,
30}
31
32impl AchievementUseCases {
33    pub fn new(
34        achievement_repo: Arc<dyn AchievementRepository>,
35        user_achievement_repo: Arc<dyn UserAchievementRepository>,
36        user_repo: Arc<dyn UserRepository>,
37    ) -> Self {
38        Self {
39            achievement_repo,
40            user_achievement_repo,
41            user_repo,
42        }
43    }
44
45    /// Create a new achievement
46    ///
47    /// # Authorization
48    /// - Only organization admins can create achievements
49    pub async fn create_achievement(
50        &self,
51        dto: CreateAchievementDto,
52    ) -> Result<AchievementResponseDto, String> {
53        let achievement = Achievement::new(
54            dto.organization_id,
55            dto.category,
56            dto.tier,
57            dto.name,
58            dto.description,
59            dto.icon,
60            dto.points_value,
61            dto.requirements,
62            dto.is_secret,
63            dto.is_repeatable,
64            dto.display_order,
65        )?;
66
67        let created = self.achievement_repo.create(&achievement).await?;
68        Ok(AchievementResponseDto::from(created))
69    }
70
71    /// Get achievement by ID
72    pub async fn get_achievement(
73        &self,
74        achievement_id: Uuid,
75    ) -> Result<AchievementResponseDto, String> {
76        let achievement = self
77            .achievement_repo
78            .find_by_id(achievement_id)
79            .await?
80            .ok_or("Achievement not found".to_string())?;
81
82        Ok(AchievementResponseDto::from(achievement))
83    }
84
85    /// List all achievements for an organization
86    pub async fn list_achievements(
87        &self,
88        organization_id: Uuid,
89    ) -> Result<Vec<AchievementResponseDto>, String> {
90        let achievements = self
91            .achievement_repo
92            .find_by_organization(organization_id)
93            .await?;
94        Ok(achievements
95            .into_iter()
96            .map(AchievementResponseDto::from)
97            .collect())
98    }
99
100    /// List achievements by category
101    pub async fn list_achievements_by_category(
102        &self,
103        organization_id: Uuid,
104        category: AchievementCategory,
105    ) -> Result<Vec<AchievementResponseDto>, String> {
106        let achievements = self
107            .achievement_repo
108            .find_by_organization_and_category(organization_id, category)
109            .await?;
110        Ok(achievements
111            .into_iter()
112            .map(AchievementResponseDto::from)
113            .collect())
114    }
115
116    /// List visible achievements for a user (non-secret or already earned)
117    pub async fn list_visible_achievements(
118        &self,
119        organization_id: Uuid,
120        user_id: Uuid,
121    ) -> Result<Vec<AchievementResponseDto>, String> {
122        let achievements = self
123            .achievement_repo
124            .find_visible_for_user(organization_id, user_id)
125            .await?;
126        Ok(achievements
127            .into_iter()
128            .map(AchievementResponseDto::from)
129            .collect())
130    }
131
132    /// Update achievement (admin only)
133    pub async fn update_achievement(
134        &self,
135        achievement_id: Uuid,
136        dto: UpdateAchievementDto,
137    ) -> Result<AchievementResponseDto, String> {
138        let mut achievement = self
139            .achievement_repo
140            .find_by_id(achievement_id)
141            .await?
142            .ok_or("Achievement not found".to_string())?;
143
144        // Apply updates
145        if let Some(name) = dto.name {
146            achievement.update_name(name)?;
147        }
148        if let Some(description) = dto.description {
149            achievement.update_description(description)?;
150        }
151        if let Some(icon) = dto.icon {
152            achievement.update_icon(icon)?;
153        }
154        if let Some(points_value) = dto.points_value {
155            achievement.update_points_value(points_value)?;
156        }
157        if let Some(requirements) = dto.requirements {
158            achievement.update_requirements(requirements)?;
159        }
160        if let Some(is_secret) = dto.is_secret {
161            achievement.is_secret = is_secret;
162        }
163        if let Some(is_repeatable) = dto.is_repeatable {
164            achievement.is_repeatable = is_repeatable;
165        }
166        if let Some(display_order) = dto.display_order {
167            achievement.display_order = display_order;
168        }
169
170        let updated = self.achievement_repo.update(&achievement).await?;
171        Ok(AchievementResponseDto::from(updated))
172    }
173
174    /// Delete achievement (admin only)
175    pub async fn delete_achievement(&self, achievement_id: Uuid) -> Result<(), String> {
176        self.achievement_repo.delete(achievement_id).await
177    }
178
179    /// Award achievement to user
180    ///
181    /// For repeatable achievements, increments times_earned counter.
182    /// For non-repeatable, returns error if already earned.
183    pub async fn award_achievement(
184        &self,
185        user_id: Uuid,
186        achievement_id: Uuid,
187        progress_data: Option<String>,
188    ) -> Result<UserAchievementResponseDto, String> {
189        // Fetch achievement
190        let achievement = self
191            .achievement_repo
192            .find_by_id(achievement_id)
193            .await?
194            .ok_or("Achievement not found".to_string())?;
195
196        // Check if already earned
197        if let Some(mut existing) = self
198            .user_achievement_repo
199            .find_by_user_and_achievement(user_id, achievement_id)
200            .await?
201        {
202            if !achievement.is_repeatable {
203                return Err("Achievement already earned and not repeatable".to_string());
204            }
205
206            // Increment times_earned for repeatable achievements
207            existing.repeat_earn()?;
208            let updated = self.user_achievement_repo.update(&existing).await?;
209            return Ok(UserAchievementResponseDto::from_entities(
210                updated,
211                achievement,
212            ));
213        }
214
215        // Award new achievement
216        let user_achievement = UserAchievement::new(user_id, achievement_id, progress_data);
217        let created = self.user_achievement_repo.create(&user_achievement).await?;
218
219        Ok(UserAchievementResponseDto::from_entities(
220            created,
221            achievement,
222        ))
223    }
224
225    /// Get all achievements earned by a user (enriched with achievement data)
226    pub async fn get_user_achievements(
227        &self,
228        user_id: Uuid,
229    ) -> Result<Vec<UserAchievementResponseDto>, String> {
230        let user_achievements = self.user_achievement_repo.find_by_user(user_id).await?;
231
232        // Enrich with achievement data
233        let mut enriched = Vec::new();
234        for ua in user_achievements {
235            if let Some(achievement) = self.achievement_repo.find_by_id(ua.achievement_id).await? {
236                enriched.push(UserAchievementResponseDto::from_entities(ua, achievement));
237            }
238        }
239
240        Ok(enriched)
241    }
242
243    /// Get recent achievements for a user (last N)
244    pub async fn get_recent_achievements(
245        &self,
246        user_id: Uuid,
247        limit: i64,
248    ) -> Result<Vec<UserAchievementResponseDto>, String> {
249        let user_achievements = self
250            .user_achievement_repo
251            .find_recent_by_user(user_id, limit)
252            .await?;
253
254        // Enrich with achievement data
255        let mut enriched = Vec::new();
256        for ua in user_achievements {
257            if let Some(achievement) = self.achievement_repo.find_by_id(ua.achievement_id).await? {
258                enriched.push(UserAchievementResponseDto::from_entities(ua, achievement));
259            }
260        }
261
262        Ok(enriched)
263    }
264}
265
266// ============================================================================
267// Challenge Use Cases
268// ============================================================================
269
270/// Use cases for challenge operations
271///
272/// Orchestrates business logic for challenge CRUD, activation/completion,
273/// and progress tracking.
274pub struct ChallengeUseCases {
275    challenge_repo: Arc<dyn ChallengeRepository>,
276    progress_repo: Arc<dyn ChallengeProgressRepository>,
277}
278
279impl ChallengeUseCases {
280    pub fn new(
281        challenge_repo: Arc<dyn ChallengeRepository>,
282        progress_repo: Arc<dyn ChallengeProgressRepository>,
283    ) -> Self {
284        Self {
285            challenge_repo,
286            progress_repo,
287        }
288    }
289
290    /// Create a new challenge (Draft status)
291    ///
292    /// # Authorization
293    /// - Only organization admins can create challenges
294    pub async fn create_challenge(
295        &self,
296        dto: CreateChallengeDto,
297    ) -> Result<ChallengeResponseDto, String> {
298        let challenge = Challenge::new(
299            dto.organization_id,
300            dto.building_id,
301            dto.challenge_type,
302            dto.title,
303            dto.description,
304            dto.icon,
305            dto.start_date,
306            dto.end_date,
307            dto.target_metric,
308            dto.target_value,
309            dto.reward_points,
310        )?;
311
312        let created = self.challenge_repo.create(&challenge).await?;
313        Ok(ChallengeResponseDto::from(created))
314    }
315
316    /// Get challenge by ID
317    pub async fn get_challenge(&self, challenge_id: Uuid) -> Result<ChallengeResponseDto, String> {
318        let challenge = self
319            .challenge_repo
320            .find_by_id(challenge_id)
321            .await?
322            .ok_or("Challenge not found".to_string())?;
323
324        Ok(ChallengeResponseDto::from(challenge))
325    }
326
327    /// List all challenges for an organization
328    pub async fn list_challenges(
329        &self,
330        organization_id: Uuid,
331    ) -> Result<Vec<ChallengeResponseDto>, String> {
332        let challenges = self
333            .challenge_repo
334            .find_by_organization(organization_id)
335            .await?;
336        Ok(challenges
337            .into_iter()
338            .map(ChallengeResponseDto::from)
339            .collect())
340    }
341
342    /// List challenges by status
343    pub async fn list_challenges_by_status(
344        &self,
345        organization_id: Uuid,
346        status: ChallengeStatus,
347    ) -> Result<Vec<ChallengeResponseDto>, String> {
348        let challenges = self
349            .challenge_repo
350            .find_by_organization_and_status(organization_id, status)
351            .await?;
352        Ok(challenges
353            .into_iter()
354            .map(ChallengeResponseDto::from)
355            .collect())
356    }
357
358    /// List challenges for a building
359    pub async fn list_building_challenges(
360        &self,
361        building_id: Uuid,
362    ) -> Result<Vec<ChallengeResponseDto>, String> {
363        let challenges = self.challenge_repo.find_by_building(building_id).await?;
364        Ok(challenges
365            .into_iter()
366            .map(ChallengeResponseDto::from)
367            .collect())
368    }
369
370    /// List active challenges (Active status + date range)
371    pub async fn list_active_challenges(
372        &self,
373        organization_id: Uuid,
374    ) -> Result<Vec<ChallengeResponseDto>, String> {
375        let challenges = self.challenge_repo.find_active(organization_id).await?;
376        Ok(challenges
377            .into_iter()
378            .map(ChallengeResponseDto::from)
379            .collect())
380    }
381
382    /// Update challenge (Draft only)
383    ///
384    /// # Authorization
385    /// - Only organization admins can update challenges
386    /// - Can only update Draft challenges
387    pub async fn update_challenge(
388        &self,
389        challenge_id: Uuid,
390        dto: UpdateChallengeDto,
391    ) -> Result<ChallengeResponseDto, String> {
392        let mut challenge = self
393            .challenge_repo
394            .find_by_id(challenge_id)
395            .await?
396            .ok_or("Challenge not found".to_string())?;
397
398        // Only Draft challenges can be updated
399        if challenge.status != ChallengeStatus::Draft {
400            return Err("Can only update Draft challenges".to_string());
401        }
402
403        // Apply updates
404        if let Some(title) = dto.title {
405            challenge.update_title(title)?;
406        }
407        if let Some(description) = dto.description {
408            challenge.update_description(description)?;
409        }
410        if let Some(icon) = dto.icon {
411            challenge.update_icon(icon)?;
412        }
413        if let Some(start_date) = dto.start_date {
414            challenge.update_start_date(start_date)?;
415        }
416        if let Some(end_date) = dto.end_date {
417            challenge.update_end_date(end_date)?;
418        }
419        if let Some(target_value) = dto.target_value {
420            challenge.update_target_value(target_value)?;
421        }
422        if let Some(reward_points) = dto.reward_points {
423            challenge.update_reward_points(reward_points)?;
424        }
425
426        let updated = self.challenge_repo.update(&challenge).await?;
427        Ok(ChallengeResponseDto::from(updated))
428    }
429
430    /// Activate challenge (Draft → Active)
431    ///
432    /// # Authorization
433    /// - Only organization admins can activate challenges
434    pub async fn activate_challenge(
435        &self,
436        challenge_id: Uuid,
437    ) -> Result<ChallengeResponseDto, String> {
438        let mut challenge = self
439            .challenge_repo
440            .find_by_id(challenge_id)
441            .await?
442            .ok_or("Challenge not found".to_string())?;
443
444        challenge.activate()?;
445        let updated = self.challenge_repo.update(&challenge).await?;
446        Ok(ChallengeResponseDto::from(updated))
447    }
448
449    /// Complete challenge (Active → Completed)
450    ///
451    /// # Authorization
452    /// - Only organization admins can complete challenges
453    pub async fn complete_challenge(
454        &self,
455        challenge_id: Uuid,
456    ) -> Result<ChallengeResponseDto, String> {
457        let mut challenge = self
458            .challenge_repo
459            .find_by_id(challenge_id)
460            .await?
461            .ok_or("Challenge not found".to_string())?;
462
463        challenge.complete()?;
464        let updated = self.challenge_repo.update(&challenge).await?;
465        Ok(ChallengeResponseDto::from(updated))
466    }
467
468    /// Cancel challenge (Draft/Active → Cancelled)
469    ///
470    /// # Authorization
471    /// - Only organization admins can cancel challenges
472    pub async fn cancel_challenge(
473        &self,
474        challenge_id: Uuid,
475    ) -> Result<ChallengeResponseDto, String> {
476        let mut challenge = self
477            .challenge_repo
478            .find_by_id(challenge_id)
479            .await?
480            .ok_or("Challenge not found".to_string())?;
481
482        challenge.cancel()?;
483        let updated = self.challenge_repo.update(&challenge).await?;
484        Ok(ChallengeResponseDto::from(updated))
485    }
486
487    /// Delete challenge (admin only)
488    pub async fn delete_challenge(&self, challenge_id: Uuid) -> Result<(), String> {
489        self.challenge_repo.delete(challenge_id).await
490    }
491
492    /// Get user progress for a challenge
493    pub async fn get_challenge_progress(
494        &self,
495        user_id: Uuid,
496        challenge_id: Uuid,
497    ) -> Result<ChallengeProgressResponseDto, String> {
498        let progress = self
499            .progress_repo
500            .find_by_user_and_challenge(user_id, challenge_id)
501            .await?
502            .ok_or("Progress not found".to_string())?;
503
504        let challenge = self
505            .challenge_repo
506            .find_by_id(challenge_id)
507            .await?
508            .ok_or("Challenge not found".to_string())?;
509
510        Ok(ChallengeProgressResponseDto::from_entities(
511            progress, challenge,
512        ))
513    }
514
515    /// List all progress for a challenge
516    pub async fn list_challenge_progress(
517        &self,
518        challenge_id: Uuid,
519    ) -> Result<Vec<ChallengeProgressResponseDto>, String> {
520        let progress_list = self.progress_repo.find_by_challenge(challenge_id).await?;
521
522        let challenge = self
523            .challenge_repo
524            .find_by_id(challenge_id)
525            .await?
526            .ok_or("Challenge not found".to_string())?;
527
528        Ok(progress_list
529            .into_iter()
530            .map(|p| ChallengeProgressResponseDto::from_entities(p, challenge.clone()))
531            .collect())
532    }
533
534    /// List active challenges for a user with progress
535    pub async fn list_user_active_progress(
536        &self,
537        user_id: Uuid,
538    ) -> Result<Vec<ChallengeProgressResponseDto>, String> {
539        let progress_list = self.progress_repo.find_active_by_user(user_id).await?;
540
541        // Enrich with challenge data
542        let mut enriched = Vec::new();
543        for progress in progress_list {
544            if let Some(challenge) = self
545                .challenge_repo
546                .find_by_id(progress.challenge_id)
547                .await?
548            {
549                enriched.push(ChallengeProgressResponseDto::from_entities(
550                    progress, challenge,
551                ));
552            }
553        }
554
555        Ok(enriched)
556    }
557
558    /// Increment user progress for a challenge
559    ///
560    /// Creates progress if doesn't exist, increments if exists.
561    /// Automatically completes challenge if target reached.
562    pub async fn increment_progress(
563        &self,
564        user_id: Uuid,
565        challenge_id: Uuid,
566        increment: i32,
567    ) -> Result<ChallengeProgressResponseDto, String> {
568        let challenge = self
569            .challenge_repo
570            .find_by_id(challenge_id)
571            .await?
572            .ok_or("Challenge not found".to_string())?;
573
574        // Get or create progress
575        let mut progress = match self
576            .progress_repo
577            .find_by_user_and_challenge(user_id, challenge_id)
578            .await?
579        {
580            Some(p) => p,
581            None => {
582                let new_progress = ChallengeProgress::new(challenge_id, user_id);
583                self.progress_repo.create(&new_progress).await?
584            }
585        };
586
587        // Increment progress
588        progress.increment(increment)?;
589
590        // Check if completed
591        if progress.current_value >= challenge.target_value && !progress.completed {
592            progress.mark_completed()?;
593        }
594
595        let updated = self.progress_repo.update(&progress).await?;
596        Ok(ChallengeProgressResponseDto::from_entities(
597            updated, challenge,
598        ))
599    }
600}
601
602// ============================================================================
603// Gamification Stats Use Cases
604// ============================================================================
605
606/// Use cases for gamification statistics and leaderboards
607///
608/// Orchestrates complex queries across achievements, challenges, and user data.
609pub struct GamificationStatsUseCases {
610    achievement_repo: Arc<dyn AchievementRepository>,
611    user_achievement_repo: Arc<dyn UserAchievementRepository>,
612    #[allow(dead_code)]
613    challenge_repo: Arc<dyn ChallengeRepository>,
614    progress_repo: Arc<dyn ChallengeProgressRepository>,
615    user_repo: Arc<dyn UserRepository>,
616}
617
618impl GamificationStatsUseCases {
619    pub fn new(
620        achievement_repo: Arc<dyn AchievementRepository>,
621        user_achievement_repo: Arc<dyn UserAchievementRepository>,
622        challenge_repo: Arc<dyn ChallengeRepository>,
623        progress_repo: Arc<dyn ChallengeProgressRepository>,
624        user_repo: Arc<dyn UserRepository>,
625    ) -> Self {
626        Self {
627            achievement_repo,
628            user_achievement_repo,
629            challenge_repo,
630            progress_repo,
631            user_repo,
632        }
633    }
634
635    /// Get comprehensive gamification stats for a user
636    pub async fn get_user_stats(
637        &self,
638        user_id: Uuid,
639        organization_id: Uuid,
640    ) -> Result<UserGamificationStatsDto, String> {
641        // Calculate achievement points
642        let total_points = self
643            .user_achievement_repo
644            .calculate_total_points(user_id)
645            .await?;
646
647        // Count achievements
648        let achievements_earned = self.user_achievement_repo.count_by_user(user_id).await? as i32;
649        let achievements_available = self
650            .achievement_repo
651            .count_by_organization(organization_id)
652            .await? as i32;
653
654        // Count challenges
655        let challenges_completed =
656            self.progress_repo.count_completed_by_user(user_id).await? as i32;
657        let challenges_active = self.progress_repo.find_active_by_user(user_id).await?.len() as i32;
658
659        // Get recent achievements (last 5)
660        let recent = self
661            .user_achievement_repo
662            .find_recent_by_user(user_id, 5)
663            .await?;
664        let mut recent_achievements = Vec::new();
665        for ua in recent {
666            if let Some(achievement) = self.achievement_repo.find_by_id(ua.achievement_id).await? {
667                recent_achievements
668                    .push(UserAchievementResponseDto::from_entities(ua, achievement));
669            }
670        }
671
672        // Calculate rank from leaderboard
673        // Get leaderboard with large limit to find user's rank
674        let leaderboard = self.get_leaderboard(organization_id, None, 10000).await?;
675        let rank = leaderboard
676            .entries
677            .iter()
678            .find(|entry| entry.user_id == user_id)
679            .map(|entry| entry.rank);
680
681        Ok(UserGamificationStatsDto {
682            user_id,
683            total_points,
684            achievements_earned,
685            achievements_available,
686            challenges_completed,
687            challenges_active,
688            rank,
689            recent_achievements,
690        })
691    }
692
693    /// Get leaderboard for an organization or building
694    pub async fn get_leaderboard(
695        &self,
696        organization_id: Uuid,
697        building_id: Option<Uuid>,
698        limit: i64,
699    ) -> Result<LeaderboardResponseDto, String> {
700        // Get leaderboard data from challenge progress (points from completed challenges)
701        let leaderboard_data = self
702            .progress_repo
703            .get_leaderboard(organization_id, building_id, limit)
704            .await?;
705
706        // Enrich with user data
707        let mut entries = Vec::new();
708        let mut rank = 1;
709        for (user_id, challenge_points) in leaderboard_data {
710            // Get achievement points
711            let achievement_points = self
712                .user_achievement_repo
713                .calculate_total_points(user_id)
714                .await?;
715
716            // Get counts
717            let achievements_count =
718                self.user_achievement_repo.count_by_user(user_id).await? as i32;
719            let challenges_completed =
720                self.progress_repo.count_completed_by_user(user_id).await? as i32;
721
722            // Get username
723            let username = if let Some(user) = self.user_repo.find_by_id(user_id).await? {
724                format!("{} {}", user.first_name, user.last_name)
725            } else {
726                "Unknown User".to_string()
727            };
728
729            entries.push(LeaderboardEntryDto {
730                user_id,
731                username,
732                total_points: achievement_points + challenge_points,
733                achievements_count,
734                challenges_completed,
735                rank,
736            });
737
738            rank += 1;
739        }
740
741        Ok(LeaderboardResponseDto {
742            organization_id,
743            building_id,
744            entries,
745            total_users: rank - 1,
746        })
747    }
748}