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(category) = dto.category {
152            achievement.category = category;
153        }
154        if let Some(tier) = dto.tier {
155            achievement.tier = tier;
156        }
157        if let Some(icon) = dto.icon {
158            achievement.update_icon(icon)?;
159        }
160        if let Some(points_value) = dto.points_value {
161            achievement.update_points_value(points_value)?;
162        }
163        if let Some(requirements) = dto.requirements {
164            achievement.update_requirements(requirements)?;
165        }
166        if let Some(is_secret) = dto.is_secret {
167            achievement.is_secret = is_secret;
168        }
169        if let Some(is_repeatable) = dto.is_repeatable {
170            achievement.is_repeatable = is_repeatable;
171        }
172        if let Some(display_order) = dto.display_order {
173            achievement.display_order = display_order;
174        }
175
176        let updated = self.achievement_repo.update(&achievement).await?;
177        Ok(AchievementResponseDto::from(updated))
178    }
179
180    /// Delete achievement (admin only)
181    pub async fn delete_achievement(&self, achievement_id: Uuid) -> Result<(), String> {
182        self.achievement_repo.delete(achievement_id).await
183    }
184
185    /// Award achievement to user
186    ///
187    /// For repeatable achievements, increments times_earned counter.
188    /// For non-repeatable, returns error if already earned.
189    pub async fn award_achievement(
190        &self,
191        user_id: Uuid,
192        achievement_id: Uuid,
193        progress_data: Option<String>,
194    ) -> Result<UserAchievementResponseDto, String> {
195        // Fetch achievement
196        let achievement = self
197            .achievement_repo
198            .find_by_id(achievement_id)
199            .await?
200            .ok_or("Achievement not found".to_string())?;
201
202        // Check if already earned
203        if let Some(mut existing) = self
204            .user_achievement_repo
205            .find_by_user_and_achievement(user_id, achievement_id)
206            .await?
207        {
208            if !achievement.is_repeatable {
209                return Err("Achievement already earned and not repeatable".to_string());
210            }
211
212            // Increment times_earned for repeatable achievements
213            existing.repeat_earn()?;
214            let updated = self.user_achievement_repo.update(&existing).await?;
215            return Ok(UserAchievementResponseDto::from_entities(
216                updated,
217                achievement,
218            ));
219        }
220
221        // Award new achievement
222        let user_achievement = UserAchievement::new(user_id, achievement_id, progress_data);
223        let created = self.user_achievement_repo.create(&user_achievement).await?;
224
225        Ok(UserAchievementResponseDto::from_entities(
226            created,
227            achievement,
228        ))
229    }
230
231    /// Get all achievements earned by a user (enriched with achievement data)
232    pub async fn get_user_achievements(
233        &self,
234        user_id: Uuid,
235    ) -> Result<Vec<UserAchievementResponseDto>, String> {
236        let user_achievements = self.user_achievement_repo.find_by_user(user_id).await?;
237
238        // Enrich with achievement data
239        let mut enriched = Vec::new();
240        for ua in user_achievements {
241            if let Some(achievement) = self.achievement_repo.find_by_id(ua.achievement_id).await? {
242                enriched.push(UserAchievementResponseDto::from_entities(ua, achievement));
243            }
244        }
245
246        Ok(enriched)
247    }
248
249    /// Get recent achievements for a user (last N)
250    pub async fn get_recent_achievements(
251        &self,
252        user_id: Uuid,
253        limit: i64,
254    ) -> Result<Vec<UserAchievementResponseDto>, String> {
255        let user_achievements = self
256            .user_achievement_repo
257            .find_recent_by_user(user_id, limit)
258            .await?;
259
260        // Enrich with achievement data
261        let mut enriched = Vec::new();
262        for ua in user_achievements {
263            if let Some(achievement) = self.achievement_repo.find_by_id(ua.achievement_id).await? {
264                enriched.push(UserAchievementResponseDto::from_entities(ua, achievement));
265            }
266        }
267
268        Ok(enriched)
269    }
270}
271
272// ============================================================================
273// Challenge Use Cases
274// ============================================================================
275
276/// Use cases for challenge operations
277///
278/// Orchestrates business logic for challenge CRUD, activation/completion,
279/// and progress tracking.
280pub struct ChallengeUseCases {
281    challenge_repo: Arc<dyn ChallengeRepository>,
282    progress_repo: Arc<dyn ChallengeProgressRepository>,
283}
284
285impl ChallengeUseCases {
286    pub fn new(
287        challenge_repo: Arc<dyn ChallengeRepository>,
288        progress_repo: Arc<dyn ChallengeProgressRepository>,
289    ) -> Self {
290        Self {
291            challenge_repo,
292            progress_repo,
293        }
294    }
295
296    /// Create a new challenge (Draft status)
297    ///
298    /// # Authorization
299    /// - Only organization admins can create challenges
300    pub async fn create_challenge(
301        &self,
302        dto: CreateChallengeDto,
303    ) -> Result<ChallengeResponseDto, String> {
304        let challenge = Challenge::new(
305            dto.organization_id,
306            dto.building_id,
307            dto.challenge_type,
308            dto.title,
309            dto.description,
310            dto.icon,
311            dto.start_date,
312            dto.end_date,
313            dto.target_metric,
314            dto.target_value,
315            dto.reward_points,
316        )?;
317
318        let created = self.challenge_repo.create(&challenge).await?;
319        Ok(ChallengeResponseDto::from(created))
320    }
321
322    /// Get challenge by ID
323    pub async fn get_challenge(&self, challenge_id: Uuid) -> Result<ChallengeResponseDto, String> {
324        let challenge = self
325            .challenge_repo
326            .find_by_id(challenge_id)
327            .await?
328            .ok_or("Challenge not found".to_string())?;
329
330        Ok(ChallengeResponseDto::from(challenge))
331    }
332
333    /// List all challenges for an organization
334    pub async fn list_challenges(
335        &self,
336        organization_id: Uuid,
337    ) -> Result<Vec<ChallengeResponseDto>, String> {
338        let challenges = self
339            .challenge_repo
340            .find_by_organization(organization_id)
341            .await?;
342        Ok(challenges
343            .into_iter()
344            .map(ChallengeResponseDto::from)
345            .collect())
346    }
347
348    /// List challenges by status
349    pub async fn list_challenges_by_status(
350        &self,
351        organization_id: Uuid,
352        status: ChallengeStatus,
353    ) -> Result<Vec<ChallengeResponseDto>, String> {
354        let challenges = self
355            .challenge_repo
356            .find_by_organization_and_status(organization_id, status)
357            .await?;
358        Ok(challenges
359            .into_iter()
360            .map(ChallengeResponseDto::from)
361            .collect())
362    }
363
364    /// List challenges for a building
365    pub async fn list_building_challenges(
366        &self,
367        building_id: Uuid,
368    ) -> Result<Vec<ChallengeResponseDto>, String> {
369        let challenges = self.challenge_repo.find_by_building(building_id).await?;
370        Ok(challenges
371            .into_iter()
372            .map(ChallengeResponseDto::from)
373            .collect())
374    }
375
376    /// List active challenges (Active status + date range)
377    pub async fn list_active_challenges(
378        &self,
379        organization_id: Uuid,
380    ) -> Result<Vec<ChallengeResponseDto>, String> {
381        let challenges = self.challenge_repo.find_active(organization_id).await?;
382        Ok(challenges
383            .into_iter()
384            .map(ChallengeResponseDto::from)
385            .collect())
386    }
387
388    /// Update challenge (Draft only)
389    ///
390    /// # Authorization
391    /// - Only organization admins can update challenges
392    /// - Can only update Draft challenges
393    pub async fn update_challenge(
394        &self,
395        challenge_id: Uuid,
396        dto: UpdateChallengeDto,
397    ) -> Result<ChallengeResponseDto, String> {
398        let mut challenge = self
399            .challenge_repo
400            .find_by_id(challenge_id)
401            .await?
402            .ok_or("Challenge not found".to_string())?;
403
404        // Only Draft challenges can be updated
405        if challenge.status != ChallengeStatus::Draft {
406            return Err("Can only update Draft challenges".to_string());
407        }
408
409        // Apply updates
410        if let Some(title) = dto.title {
411            challenge.update_title(title)?;
412        }
413        if let Some(description) = dto.description {
414            challenge.update_description(description)?;
415        }
416        if let Some(icon) = dto.icon {
417            challenge.update_icon(icon)?;
418        }
419        if let Some(start_date) = dto.start_date {
420            challenge.update_start_date(start_date)?;
421        }
422        if let Some(end_date) = dto.end_date {
423            challenge.update_end_date(end_date)?;
424        }
425        if let Some(target_value) = dto.target_value {
426            challenge.update_target_value(target_value)?;
427        }
428        if let Some(reward_points) = dto.reward_points {
429            challenge.update_reward_points(reward_points)?;
430        }
431
432        let updated = self.challenge_repo.update(&challenge).await?;
433        Ok(ChallengeResponseDto::from(updated))
434    }
435
436    /// Activate challenge (Draft → Active)
437    ///
438    /// # Authorization
439    /// - Only organization admins can activate challenges
440    pub async fn activate_challenge(
441        &self,
442        challenge_id: Uuid,
443    ) -> Result<ChallengeResponseDto, String> {
444        let mut challenge = self
445            .challenge_repo
446            .find_by_id(challenge_id)
447            .await?
448            .ok_or("Challenge not found".to_string())?;
449
450        challenge.activate()?;
451        let updated = self.challenge_repo.update(&challenge).await?;
452        Ok(ChallengeResponseDto::from(updated))
453    }
454
455    /// Complete challenge (Active → Completed)
456    ///
457    /// # Authorization
458    /// - Only organization admins can complete challenges
459    pub async fn complete_challenge(
460        &self,
461        challenge_id: Uuid,
462    ) -> Result<ChallengeResponseDto, String> {
463        let mut challenge = self
464            .challenge_repo
465            .find_by_id(challenge_id)
466            .await?
467            .ok_or("Challenge not found".to_string())?;
468
469        challenge.complete()?;
470        let updated = self.challenge_repo.update(&challenge).await?;
471        Ok(ChallengeResponseDto::from(updated))
472    }
473
474    /// Cancel challenge (Draft/Active → Cancelled)
475    ///
476    /// # Authorization
477    /// - Only organization admins can cancel challenges
478    pub async fn cancel_challenge(
479        &self,
480        challenge_id: Uuid,
481    ) -> Result<ChallengeResponseDto, String> {
482        let mut challenge = self
483            .challenge_repo
484            .find_by_id(challenge_id)
485            .await?
486            .ok_or("Challenge not found".to_string())?;
487
488        challenge.cancel()?;
489        let updated = self.challenge_repo.update(&challenge).await?;
490        Ok(ChallengeResponseDto::from(updated))
491    }
492
493    /// Delete challenge (admin only)
494    pub async fn delete_challenge(&self, challenge_id: Uuid) -> Result<(), String> {
495        self.challenge_repo.delete(challenge_id).await
496    }
497
498    /// Get user progress for a challenge
499    pub async fn get_challenge_progress(
500        &self,
501        user_id: Uuid,
502        challenge_id: Uuid,
503    ) -> Result<ChallengeProgressResponseDto, String> {
504        let progress = self
505            .progress_repo
506            .find_by_user_and_challenge(user_id, challenge_id)
507            .await?
508            .ok_or("Progress not found".to_string())?;
509
510        let challenge = self
511            .challenge_repo
512            .find_by_id(challenge_id)
513            .await?
514            .ok_or("Challenge not found".to_string())?;
515
516        Ok(ChallengeProgressResponseDto::from_entities(
517            progress, challenge,
518        ))
519    }
520
521    /// List all progress for a challenge
522    pub async fn list_challenge_progress(
523        &self,
524        challenge_id: Uuid,
525    ) -> Result<Vec<ChallengeProgressResponseDto>, String> {
526        let progress_list = self.progress_repo.find_by_challenge(challenge_id).await?;
527
528        let challenge = self
529            .challenge_repo
530            .find_by_id(challenge_id)
531            .await?
532            .ok_or("Challenge not found".to_string())?;
533
534        Ok(progress_list
535            .into_iter()
536            .map(|p| ChallengeProgressResponseDto::from_entities(p, challenge.clone()))
537            .collect())
538    }
539
540    /// List active challenges for a user with progress
541    pub async fn list_user_active_progress(
542        &self,
543        user_id: Uuid,
544    ) -> Result<Vec<ChallengeProgressResponseDto>, String> {
545        let progress_list = self.progress_repo.find_active_by_user(user_id).await?;
546
547        // Enrich with challenge data
548        let mut enriched = Vec::new();
549        for progress in progress_list {
550            if let Some(challenge) = self
551                .challenge_repo
552                .find_by_id(progress.challenge_id)
553                .await?
554            {
555                enriched.push(ChallengeProgressResponseDto::from_entities(
556                    progress, challenge,
557                ));
558            }
559        }
560
561        Ok(enriched)
562    }
563
564    /// Increment user progress for a challenge
565    ///
566    /// Creates progress if doesn't exist, increments if exists.
567    /// Automatically completes challenge if target reached.
568    pub async fn increment_progress(
569        &self,
570        user_id: Uuid,
571        challenge_id: Uuid,
572        increment: i32,
573    ) -> Result<ChallengeProgressResponseDto, String> {
574        let challenge = self
575            .challenge_repo
576            .find_by_id(challenge_id)
577            .await?
578            .ok_or("Challenge not found".to_string())?;
579
580        // Get or create progress
581        let mut progress = match self
582            .progress_repo
583            .find_by_user_and_challenge(user_id, challenge_id)
584            .await?
585        {
586            Some(p) => p,
587            None => {
588                let new_progress = ChallengeProgress::new(challenge_id, user_id);
589                self.progress_repo.create(&new_progress).await?
590            }
591        };
592
593        // Increment progress
594        progress.increment(increment)?;
595
596        // Check if completed
597        if progress.current_value >= challenge.target_value && !progress.completed {
598            progress.mark_completed()?;
599        }
600
601        let updated = self.progress_repo.update(&progress).await?;
602        Ok(ChallengeProgressResponseDto::from_entities(
603            updated, challenge,
604        ))
605    }
606}
607
608// ============================================================================
609// Gamification Stats Use Cases
610// ============================================================================
611
612/// Use cases for gamification statistics and leaderboards
613///
614/// Orchestrates complex queries across achievements, challenges, and user data.
615pub struct GamificationStatsUseCases {
616    achievement_repo: Arc<dyn AchievementRepository>,
617    user_achievement_repo: Arc<dyn UserAchievementRepository>,
618    #[allow(dead_code)]
619    challenge_repo: Arc<dyn ChallengeRepository>,
620    progress_repo: Arc<dyn ChallengeProgressRepository>,
621    user_repo: Arc<dyn UserRepository>,
622}
623
624impl GamificationStatsUseCases {
625    pub fn new(
626        achievement_repo: Arc<dyn AchievementRepository>,
627        user_achievement_repo: Arc<dyn UserAchievementRepository>,
628        challenge_repo: Arc<dyn ChallengeRepository>,
629        progress_repo: Arc<dyn ChallengeProgressRepository>,
630        user_repo: Arc<dyn UserRepository>,
631    ) -> Self {
632        Self {
633            achievement_repo,
634            user_achievement_repo,
635            challenge_repo,
636            progress_repo,
637            user_repo,
638        }
639    }
640
641    /// Get comprehensive gamification stats for a user
642    pub async fn get_user_stats(
643        &self,
644        user_id: Uuid,
645        organization_id: Uuid,
646    ) -> Result<UserGamificationStatsDto, String> {
647        // Calculate achievement points
648        let achievement_points = self
649            .user_achievement_repo
650            .calculate_total_points(user_id)
651            .await?;
652
653        // Count achievements
654        let achievements_earned = self.user_achievement_repo.count_by_user(user_id).await? as i32;
655        let achievements_available = self
656            .achievement_repo
657            .count_by_organization(organization_id)
658            .await? as i32;
659
660        // Count challenges
661        let challenges_completed =
662            self.progress_repo.count_completed_by_user(user_id).await? as i32;
663        let challenges_active = self.progress_repo.find_active_by_user(user_id).await?.len() as i32;
664
665        // Get recent achievements (last 5)
666        let recent = self
667            .user_achievement_repo
668            .find_recent_by_user(user_id, 5)
669            .await?;
670        let mut recent_achievements = Vec::new();
671        for ua in recent {
672            if let Some(achievement) = self.achievement_repo.find_by_id(ua.achievement_id).await? {
673                recent_achievements
674                    .push(UserAchievementResponseDto::from_entities(ua, achievement));
675            }
676        }
677
678        // Calculate rank and total points from leaderboard (includes achievements + challenges)
679        let leaderboard = self.get_leaderboard(organization_id, None, 10000).await?;
680        let leaderboard_entry = leaderboard
681            .entries
682            .iter()
683            .find(|entry| entry.user_id == user_id);
684        let rank = leaderboard_entry.map(|entry| entry.rank);
685        let total_points = leaderboard_entry
686            .map(|entry| entry.total_points)
687            .unwrap_or(achievement_points);
688
689        Ok(UserGamificationStatsDto {
690            user_id,
691            total_points,
692            achievements_earned,
693            achievements_available,
694            challenges_completed,
695            challenges_active,
696            rank,
697            recent_achievements,
698        })
699    }
700
701    /// Get leaderboard for an organization or building
702    pub async fn get_leaderboard(
703        &self,
704        organization_id: Uuid,
705        building_id: Option<Uuid>,
706        limit: i64,
707    ) -> Result<LeaderboardResponseDto, String> {
708        // Get leaderboard data from challenge progress (points from completed challenges)
709        let leaderboard_data = self
710            .progress_repo
711            .get_leaderboard(organization_id, building_id, limit)
712            .await?;
713
714        // Enrich with user data
715        let mut entries = Vec::new();
716        let mut rank = 1;
717        for (user_id, challenge_points) in leaderboard_data {
718            // Get achievement points
719            let achievement_points = self
720                .user_achievement_repo
721                .calculate_total_points(user_id)
722                .await?;
723
724            // Get counts
725            let achievements_count =
726                self.user_achievement_repo.count_by_user(user_id).await? as i32;
727            let challenges_completed =
728                self.progress_repo.count_completed_by_user(user_id).await? as i32;
729
730            // Get username
731            let username = if let Some(user) = self.user_repo.find_by_id(user_id).await? {
732                format!("{} {}", user.first_name, user.last_name)
733            } else {
734                "Unknown User".to_string()
735            };
736
737            entries.push(LeaderboardEntryDto {
738                user_id,
739                username,
740                total_points: achievement_points + challenge_points,
741                achievements_count,
742                challenges_completed,
743                rank,
744            });
745
746            rank += 1;
747        }
748
749        Ok(LeaderboardResponseDto {
750            organization_id,
751            building_id,
752            entries,
753            total_users: rank - 1,
754        })
755    }
756}
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761    use crate::application::ports::{
762        AchievementRepository, ChallengeProgressRepository, ChallengeRepository,
763        UserAchievementRepository, UserRepository,
764    };
765    use crate::domain::entities::{
766        Achievement, AchievementCategory, AchievementTier, Challenge, ChallengeProgress,
767        ChallengeStatus, ChallengeType, User, UserAchievement, UserRole,
768    };
769    use async_trait::async_trait;
770    use chrono::{Duration, Utc};
771    use std::collections::HashMap;
772    use std::sync::{Arc, Mutex};
773    use uuid::Uuid;
774
775    // ── Mock AchievementRepository ──────────────────────────────────────────
776    struct MockAchievementRepo {
777        items: Mutex<HashMap<Uuid, Achievement>>,
778    }
779    impl MockAchievementRepo {
780        fn new() -> Self {
781            Self {
782                items: Mutex::new(HashMap::new()),
783            }
784        }
785    }
786    #[async_trait]
787    impl AchievementRepository for MockAchievementRepo {
788        async fn create(&self, a: &Achievement) -> Result<Achievement, String> {
789            self.items.lock().unwrap().insert(a.id, a.clone());
790            Ok(a.clone())
791        }
792        async fn find_by_id(&self, id: Uuid) -> Result<Option<Achievement>, String> {
793            Ok(self.items.lock().unwrap().get(&id).cloned())
794        }
795        async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<Achievement>, String> {
796            Ok(self
797                .items
798                .lock()
799                .unwrap()
800                .values()
801                .filter(|a| a.organization_id == org_id)
802                .cloned()
803                .collect())
804        }
805        async fn find_by_organization_and_category(
806            &self,
807            org_id: Uuid,
808            cat: AchievementCategory,
809        ) -> Result<Vec<Achievement>, String> {
810            Ok(self
811                .items
812                .lock()
813                .unwrap()
814                .values()
815                .filter(|a| a.organization_id == org_id && a.category == cat)
816                .cloned()
817                .collect())
818        }
819        async fn find_visible_for_user(
820            &self,
821            org_id: Uuid,
822            _user_id: Uuid,
823        ) -> Result<Vec<Achievement>, String> {
824            Ok(self
825                .items
826                .lock()
827                .unwrap()
828                .values()
829                .filter(|a| a.organization_id == org_id && !a.is_secret)
830                .cloned()
831                .collect())
832        }
833        async fn update(&self, a: &Achievement) -> Result<Achievement, String> {
834            self.items.lock().unwrap().insert(a.id, a.clone());
835            Ok(a.clone())
836        }
837        async fn delete(&self, id: Uuid) -> Result<(), String> {
838            self.items.lock().unwrap().remove(&id);
839            Ok(())
840        }
841        async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String> {
842            Ok(self
843                .items
844                .lock()
845                .unwrap()
846                .values()
847                .filter(|a| a.organization_id == org_id)
848                .count() as i64)
849        }
850    }
851
852    // ── Mock UserAchievementRepository ──────────────────────────────────────
853    struct MockUserAchievementRepo {
854        items: Mutex<HashMap<Uuid, UserAchievement>>,
855    }
856    impl MockUserAchievementRepo {
857        fn new() -> Self {
858            Self {
859                items: Mutex::new(HashMap::new()),
860            }
861        }
862    }
863    #[async_trait]
864    impl UserAchievementRepository for MockUserAchievementRepo {
865        async fn create(&self, ua: &UserAchievement) -> Result<UserAchievement, String> {
866            self.items.lock().unwrap().insert(ua.id, ua.clone());
867            Ok(ua.clone())
868        }
869        async fn find_by_id(&self, id: Uuid) -> Result<Option<UserAchievement>, String> {
870            Ok(self.items.lock().unwrap().get(&id).cloned())
871        }
872        async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<UserAchievement>, String> {
873            Ok(self
874                .items
875                .lock()
876                .unwrap()
877                .values()
878                .filter(|u| u.user_id == user_id)
879                .cloned()
880                .collect())
881        }
882        async fn find_by_user_and_achievement(
883            &self,
884            user_id: Uuid,
885            achievement_id: Uuid,
886        ) -> Result<Option<UserAchievement>, String> {
887            Ok(self
888                .items
889                .lock()
890                .unwrap()
891                .values()
892                .find(|u| u.user_id == user_id && u.achievement_id == achievement_id)
893                .cloned())
894        }
895        async fn update(&self, ua: &UserAchievement) -> Result<UserAchievement, String> {
896            self.items.lock().unwrap().insert(ua.id, ua.clone());
897            Ok(ua.clone())
898        }
899        async fn calculate_total_points(&self, _user_id: Uuid) -> Result<i32, String> {
900            Ok(0)
901        }
902        async fn count_by_user(&self, user_id: Uuid) -> Result<i64, String> {
903            Ok(self
904                .items
905                .lock()
906                .unwrap()
907                .values()
908                .filter(|u| u.user_id == user_id)
909                .count() as i64)
910        }
911        async fn find_recent_by_user(
912            &self,
913            user_id: Uuid,
914            limit: i64,
915        ) -> Result<Vec<UserAchievement>, String> {
916            let map = self.items.lock().unwrap();
917            let mut v: Vec<_> = map
918                .values()
919                .filter(|u| u.user_id == user_id)
920                .cloned()
921                .collect();
922            v.sort_by(|a, b| b.earned_at.cmp(&a.earned_at));
923            v.truncate(limit as usize);
924            Ok(v)
925        }
926    }
927
928    // ── Mock ChallengeRepository ────────────────────────────────────────────
929    struct MockChallengeRepo {
930        items: Mutex<HashMap<Uuid, Challenge>>,
931    }
932    impl MockChallengeRepo {
933        fn new() -> Self {
934            Self {
935                items: Mutex::new(HashMap::new()),
936            }
937        }
938    }
939    #[async_trait]
940    impl ChallengeRepository for MockChallengeRepo {
941        async fn create(&self, c: &Challenge) -> Result<Challenge, String> {
942            self.items.lock().unwrap().insert(c.id, c.clone());
943            Ok(c.clone())
944        }
945        async fn find_by_id(&self, id: Uuid) -> Result<Option<Challenge>, String> {
946            Ok(self.items.lock().unwrap().get(&id).cloned())
947        }
948        async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<Challenge>, String> {
949            Ok(self
950                .items
951                .lock()
952                .unwrap()
953                .values()
954                .filter(|c| c.organization_id == org_id)
955                .cloned()
956                .collect())
957        }
958        async fn find_by_organization_and_status(
959            &self,
960            org_id: Uuid,
961            status: ChallengeStatus,
962        ) -> Result<Vec<Challenge>, String> {
963            Ok(self
964                .items
965                .lock()
966                .unwrap()
967                .values()
968                .filter(|c| c.organization_id == org_id && c.status == status)
969                .cloned()
970                .collect())
971        }
972        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Challenge>, String> {
973            Ok(self
974                .items
975                .lock()
976                .unwrap()
977                .values()
978                .filter(|c| c.building_id == Some(building_id))
979                .cloned()
980                .collect())
981        }
982        async fn find_active(&self, org_id: Uuid) -> Result<Vec<Challenge>, String> {
983            let now = Utc::now();
984            Ok(self
985                .items
986                .lock()
987                .unwrap()
988                .values()
989                .filter(|c| {
990                    c.organization_id == org_id
991                        && c.status == ChallengeStatus::Active
992                        && now >= c.start_date
993                        && now < c.end_date
994                })
995                .cloned()
996                .collect())
997        }
998        async fn find_ended_not_completed(&self) -> Result<Vec<Challenge>, String> {
999            let now = Utc::now();
1000            Ok(self
1001                .items
1002                .lock()
1003                .unwrap()
1004                .values()
1005                .filter(|c| c.status == ChallengeStatus::Active && now >= c.end_date)
1006                .cloned()
1007                .collect())
1008        }
1009        async fn update(&self, c: &Challenge) -> Result<Challenge, String> {
1010            self.items.lock().unwrap().insert(c.id, c.clone());
1011            Ok(c.clone())
1012        }
1013        async fn delete(&self, id: Uuid) -> Result<(), String> {
1014            self.items.lock().unwrap().remove(&id);
1015            Ok(())
1016        }
1017        async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String> {
1018            Ok(self
1019                .items
1020                .lock()
1021                .unwrap()
1022                .values()
1023                .filter(|c| c.organization_id == org_id)
1024                .count() as i64)
1025        }
1026    }
1027
1028    // ── Mock ChallengeProgressRepository ────────────────────────────────────
1029    struct MockProgressRepo {
1030        items: Mutex<HashMap<Uuid, ChallengeProgress>>,
1031    }
1032    impl MockProgressRepo {
1033        fn new() -> Self {
1034            Self {
1035                items: Mutex::new(HashMap::new()),
1036            }
1037        }
1038    }
1039    #[async_trait]
1040    impl ChallengeProgressRepository for MockProgressRepo {
1041        async fn create(&self, p: &ChallengeProgress) -> Result<ChallengeProgress, String> {
1042            self.items.lock().unwrap().insert(p.id, p.clone());
1043            Ok(p.clone())
1044        }
1045        async fn find_by_id(&self, id: Uuid) -> Result<Option<ChallengeProgress>, String> {
1046            Ok(self.items.lock().unwrap().get(&id).cloned())
1047        }
1048        async fn find_by_user_and_challenge(
1049            &self,
1050            user_id: Uuid,
1051            challenge_id: Uuid,
1052        ) -> Result<Option<ChallengeProgress>, String> {
1053            Ok(self
1054                .items
1055                .lock()
1056                .unwrap()
1057                .values()
1058                .find(|p| p.user_id == user_id && p.challenge_id == challenge_id)
1059                .cloned())
1060        }
1061        async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ChallengeProgress>, String> {
1062            Ok(self
1063                .items
1064                .lock()
1065                .unwrap()
1066                .values()
1067                .filter(|p| p.user_id == user_id)
1068                .cloned()
1069                .collect())
1070        }
1071        async fn find_by_challenge(
1072            &self,
1073            challenge_id: Uuid,
1074        ) -> Result<Vec<ChallengeProgress>, String> {
1075            Ok(self
1076                .items
1077                .lock()
1078                .unwrap()
1079                .values()
1080                .filter(|p| p.challenge_id == challenge_id)
1081                .cloned()
1082                .collect())
1083        }
1084        async fn find_active_by_user(
1085            &self,
1086            user_id: Uuid,
1087        ) -> Result<Vec<ChallengeProgress>, String> {
1088            Ok(self
1089                .items
1090                .lock()
1091                .unwrap()
1092                .values()
1093                .filter(|p| p.user_id == user_id && !p.completed)
1094                .cloned()
1095                .collect())
1096        }
1097        async fn update(&self, p: &ChallengeProgress) -> Result<ChallengeProgress, String> {
1098            self.items.lock().unwrap().insert(p.id, p.clone());
1099            Ok(p.clone())
1100        }
1101        async fn count_completed_by_user(&self, user_id: Uuid) -> Result<i64, String> {
1102            Ok(self
1103                .items
1104                .lock()
1105                .unwrap()
1106                .values()
1107                .filter(|p| p.user_id == user_id && p.completed)
1108                .count() as i64)
1109        }
1110        async fn get_leaderboard(
1111            &self,
1112            _org_id: Uuid,
1113            _building_id: Option<Uuid>,
1114            _limit: i64,
1115        ) -> Result<Vec<(Uuid, i32)>, String> {
1116            Ok(vec![])
1117        }
1118    }
1119
1120    // ── Mock UserRepository (minimal, needed by AchievementUseCases) ────────
1121    struct MockUserRepo;
1122    #[async_trait]
1123    impl UserRepository for MockUserRepo {
1124        async fn create(&self, _u: &User) -> Result<User, String> {
1125            Err("not impl".to_string())
1126        }
1127        async fn find_by_id(&self, _id: Uuid) -> Result<Option<User>, String> {
1128            Ok(None)
1129        }
1130        async fn find_by_email(&self, _e: &str) -> Result<Option<User>, String> {
1131            Ok(None)
1132        }
1133        async fn find_all(&self) -> Result<Vec<User>, String> {
1134            Ok(vec![])
1135        }
1136        async fn find_by_organization(&self, _o: Uuid) -> Result<Vec<User>, String> {
1137            Ok(vec![])
1138        }
1139        async fn update(&self, _u: &User) -> Result<User, String> {
1140            Err("not impl".to_string())
1141        }
1142        async fn delete(&self, _id: Uuid) -> Result<bool, String> {
1143            Ok(false)
1144        }
1145        async fn count_by_organization(&self, _o: Uuid) -> Result<i64, String> {
1146            Ok(0)
1147        }
1148    }
1149
1150    // ── Helpers ─────────────────────────────────────────────────────────────
1151    fn make_achievement_dto(org_id: Uuid) -> CreateAchievementDto {
1152        CreateAchievementDto {
1153            organization_id: org_id,
1154            category: AchievementCategory::Community,
1155            tier: AchievementTier::Bronze,
1156            name: "First Booking".to_string(),
1157            description: "Made your first resource booking in the system".to_string(),
1158            icon: "star".to_string(),
1159            points_value: 10,
1160            requirements: r#"{"action": "booking_created", "count": 1}"#.to_string(),
1161            is_secret: false,
1162            is_repeatable: false,
1163            display_order: 1,
1164        }
1165    }
1166
1167    fn make_challenge_dto(org_id: Uuid) -> CreateChallengeDto {
1168        let start = Utc::now() + Duration::days(1);
1169        let end = start + Duration::days(7);
1170        CreateChallengeDto {
1171            organization_id: org_id,
1172            building_id: None,
1173            challenge_type: ChallengeType::Individual,
1174            title: "Booking Week".to_string(),
1175            description: "Make 5 resource bookings this week to earn points!".to_string(),
1176            icon: "calendar".to_string(),
1177            start_date: start,
1178            end_date: end,
1179            target_metric: "bookings_created".to_string(),
1180            target_value: 5,
1181            reward_points: 50,
1182        }
1183    }
1184
1185    fn setup_achievement_uc() -> (AchievementUseCases, Uuid) {
1186        let org_id = Uuid::new_v4();
1187        let uc = AchievementUseCases::new(
1188            Arc::new(MockAchievementRepo::new()) as Arc<dyn AchievementRepository>,
1189            Arc::new(MockUserAchievementRepo::new()) as Arc<dyn UserAchievementRepository>,
1190            Arc::new(MockUserRepo) as Arc<dyn UserRepository>,
1191        );
1192        (uc, org_id)
1193    }
1194
1195    fn setup_challenge_uc() -> (ChallengeUseCases, Uuid) {
1196        let org_id = Uuid::new_v4();
1197        let uc = ChallengeUseCases::new(
1198            Arc::new(MockChallengeRepo::new()) as Arc<dyn ChallengeRepository>,
1199            Arc::new(MockProgressRepo::new()) as Arc<dyn ChallengeProgressRepository>,
1200        );
1201        (uc, org_id)
1202    }
1203
1204    // ══════════════════════════════════════════════════════════════════════
1205    //  AchievementUseCases Tests
1206    // ══════════════════════════════════════════════════════════════════════
1207
1208    #[tokio::test]
1209    async fn test_create_achievement_success() {
1210        let (uc, org_id) = setup_achievement_uc();
1211        let dto = make_achievement_dto(org_id);
1212        let result = uc.create_achievement(dto).await;
1213        assert!(result.is_ok());
1214        let resp = result.unwrap();
1215        assert_eq!(resp.name, "First Booking");
1216        assert_eq!(resp.points_value, 10);
1217    }
1218
1219    #[tokio::test]
1220    async fn test_get_achievement_success() {
1221        let (uc, org_id) = setup_achievement_uc();
1222        let dto = make_achievement_dto(org_id);
1223        let created = uc.create_achievement(dto).await.unwrap();
1224
1225        let result = uc.get_achievement(created.id).await;
1226        assert!(result.is_ok());
1227        assert_eq!(result.unwrap().id, created.id);
1228    }
1229
1230    #[tokio::test]
1231    async fn test_get_achievement_not_found() {
1232        let (uc, _) = setup_achievement_uc();
1233        let result = uc.get_achievement(Uuid::new_v4()).await;
1234        assert!(result.is_err());
1235        assert_eq!(result.unwrap_err(), "Achievement not found");
1236    }
1237
1238    #[tokio::test]
1239    async fn test_award_achievement_success() {
1240        let (uc, org_id) = setup_achievement_uc();
1241        let dto = make_achievement_dto(org_id);
1242        let created = uc.create_achievement(dto).await.unwrap();
1243
1244        let user_id = Uuid::new_v4();
1245        let result = uc.award_achievement(user_id, created.id, None).await;
1246        assert!(result.is_ok());
1247        let resp = result.unwrap();
1248        assert_eq!(resp.user_id, user_id);
1249        assert_eq!(resp.times_earned, 1);
1250    }
1251
1252    #[tokio::test]
1253    async fn test_award_non_repeatable_twice_fails() {
1254        let (uc, org_id) = setup_achievement_uc();
1255        let dto = make_achievement_dto(org_id); // is_repeatable = false
1256        let created = uc.create_achievement(dto).await.unwrap();
1257
1258        let user_id = Uuid::new_v4();
1259        uc.award_achievement(user_id, created.id, None)
1260            .await
1261            .unwrap();
1262
1263        // Second award should fail
1264        let result = uc.award_achievement(user_id, created.id, None).await;
1265        assert!(result.is_err());
1266        assert!(result.unwrap_err().contains("not repeatable"));
1267    }
1268
1269    #[tokio::test]
1270    async fn test_award_repeatable_increments() {
1271        let (uc, org_id) = setup_achievement_uc();
1272        let mut dto = make_achievement_dto(org_id);
1273        dto.is_repeatable = true;
1274        let created = uc.create_achievement(dto).await.unwrap();
1275
1276        let user_id = Uuid::new_v4();
1277        uc.award_achievement(user_id, created.id, None)
1278            .await
1279            .unwrap();
1280
1281        // Second award should succeed and increment
1282        let result = uc.award_achievement(user_id, created.id, None).await;
1283        assert!(result.is_ok());
1284        assert_eq!(result.unwrap().times_earned, 2);
1285    }
1286
1287    #[tokio::test]
1288    async fn test_list_achievements() {
1289        let (uc, org_id) = setup_achievement_uc();
1290        let dto1 = make_achievement_dto(org_id);
1291        let mut dto2 = make_achievement_dto(org_id);
1292        dto2.name = "SEL Pioneer".to_string();
1293        dto2.description = "Completed your first SEL exchange in the system".to_string();
1294
1295        uc.create_achievement(dto1).await.unwrap();
1296        uc.create_achievement(dto2).await.unwrap();
1297
1298        let result = uc.list_achievements(org_id).await;
1299        assert!(result.is_ok());
1300        assert_eq!(result.unwrap().len(), 2);
1301    }
1302
1303    #[tokio::test]
1304    async fn test_delete_achievement() {
1305        let (uc, org_id) = setup_achievement_uc();
1306        let dto = make_achievement_dto(org_id);
1307        let created = uc.create_achievement(dto).await.unwrap();
1308
1309        let result = uc.delete_achievement(created.id).await;
1310        assert!(result.is_ok());
1311
1312        let fetch = uc.get_achievement(created.id).await;
1313        assert!(fetch.is_err());
1314    }
1315
1316    // ══════════════════════════════════════════════════════════════════════
1317    //  ChallengeUseCases Tests
1318    // ══════════════════════════════════════════════════════════════════════
1319
1320    #[tokio::test]
1321    async fn test_create_challenge_success() {
1322        let (uc, org_id) = setup_challenge_uc();
1323        let dto = make_challenge_dto(org_id);
1324        let result = uc.create_challenge(dto).await;
1325        assert!(result.is_ok());
1326        let resp = result.unwrap();
1327        assert_eq!(resp.title, "Booking Week");
1328        assert_eq!(resp.status, ChallengeStatus::Draft);
1329    }
1330
1331    #[tokio::test]
1332    async fn test_activate_challenge_success() {
1333        let (uc, org_id) = setup_challenge_uc();
1334        let dto = make_challenge_dto(org_id);
1335        let created = uc.create_challenge(dto).await.unwrap();
1336
1337        let result = uc.activate_challenge(created.id).await;
1338        assert!(result.is_ok());
1339        assert_eq!(result.unwrap().status, ChallengeStatus::Active);
1340    }
1341
1342    #[tokio::test]
1343    async fn test_activate_already_active_fails() {
1344        let (uc, org_id) = setup_challenge_uc();
1345        let dto = make_challenge_dto(org_id);
1346        let created = uc.create_challenge(dto).await.unwrap();
1347        uc.activate_challenge(created.id).await.unwrap();
1348
1349        let result = uc.activate_challenge(created.id).await;
1350        assert!(result.is_err());
1351        assert!(result.unwrap_err().contains("already active"));
1352    }
1353
1354    #[tokio::test]
1355    async fn test_cancel_challenge_success() {
1356        let (uc, org_id) = setup_challenge_uc();
1357        let dto = make_challenge_dto(org_id);
1358        let created = uc.create_challenge(dto).await.unwrap();
1359
1360        let result = uc.cancel_challenge(created.id).await;
1361        assert!(result.is_ok());
1362        assert_eq!(result.unwrap().status, ChallengeStatus::Cancelled);
1363    }
1364
1365    #[tokio::test]
1366    async fn test_challenge_not_found() {
1367        let (uc, _) = setup_challenge_uc();
1368        let result = uc.get_challenge(Uuid::new_v4()).await;
1369        assert!(result.is_err());
1370        assert_eq!(result.unwrap_err(), "Challenge not found");
1371    }
1372
1373    #[tokio::test]
1374    async fn test_increment_progress_creates_and_increments() {
1375        let (uc, org_id) = setup_challenge_uc();
1376        let dto = make_challenge_dto(org_id);
1377        let created = uc.create_challenge(dto).await.unwrap();
1378
1379        let user_id = Uuid::new_v4();
1380
1381        // Increment (auto-creates progress record)
1382        let result = uc.increment_progress(user_id, created.id, 3).await;
1383        assert!(result.is_ok());
1384        let resp = result.unwrap();
1385        assert_eq!(resp.current_value, 3);
1386        assert!(!resp.completed);
1387    }
1388
1389    #[tokio::test]
1390    async fn test_increment_progress_auto_completes() {
1391        let (uc, org_id) = setup_challenge_uc();
1392        let dto = make_challenge_dto(org_id); // target_value = 5
1393        let created = uc.create_challenge(dto).await.unwrap();
1394
1395        let user_id = Uuid::new_v4();
1396
1397        // Increment to reach target
1398        uc.increment_progress(user_id, created.id, 3).await.unwrap();
1399        let result = uc.increment_progress(user_id, created.id, 3).await; // total = 6 >= 5
1400        assert!(result.is_ok());
1401        let resp = result.unwrap();
1402        assert!(resp.completed);
1403        assert_eq!(resp.current_value, 6);
1404    }
1405
1406    #[tokio::test]
1407    async fn test_delete_challenge() {
1408        let (uc, org_id) = setup_challenge_uc();
1409        let dto = make_challenge_dto(org_id);
1410        let created = uc.create_challenge(dto).await.unwrap();
1411
1412        let result = uc.delete_challenge(created.id).await;
1413        assert!(result.is_ok());
1414
1415        let fetch = uc.get_challenge(created.id).await;
1416        assert!(fetch.is_err());
1417    }
1418}