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
17pub 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 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 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 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 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 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 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 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 pub async fn delete_achievement(&self, achievement_id: Uuid) -> Result<(), String> {
182 self.achievement_repo.delete(achievement_id).await
183 }
184
185 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 let achievement = self
197 .achievement_repo
198 .find_by_id(achievement_id)
199 .await?
200 .ok_or("Achievement not found".to_string())?;
201
202 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 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 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 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 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 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 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
272pub 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 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 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 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 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 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 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 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 if challenge.status != ChallengeStatus::Draft {
406 return Err("Can only update Draft challenges".to_string());
407 }
408
409 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 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 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 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 pub async fn delete_challenge(&self, challenge_id: Uuid) -> Result<(), String> {
495 self.challenge_repo.delete(challenge_id).await
496 }
497
498 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 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 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 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 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 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 progress.increment(increment)?;
595
596 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
608pub 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 pub async fn get_user_stats(
643 &self,
644 user_id: Uuid,
645 organization_id: Uuid,
646 ) -> Result<UserGamificationStatsDto, String> {
647 let achievement_points = self
649 .user_achievement_repo
650 .calculate_total_points(user_id)
651 .await?;
652
653 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 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 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 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 pub async fn get_leaderboard(
703 &self,
704 organization_id: Uuid,
705 building_id: Option<Uuid>,
706 limit: i64,
707 ) -> Result<LeaderboardResponseDto, String> {
708 let leaderboard_data = self
710 .progress_repo
711 .get_leaderboard(organization_id, building_id, limit)
712 .await?;
713
714 let mut entries = Vec::new();
716 let mut rank = 1;
717 for (user_id, challenge_points) in leaderboard_data {
718 let achievement_points = self
720 .user_achievement_repo
721 .calculate_total_points(user_id)
722 .await?;
723
724 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 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 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 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 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 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 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 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 #[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); 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 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 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 #[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 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); let created = uc.create_challenge(dto).await.unwrap();
1394
1395 let user_id = Uuid::new_v4();
1396
1397 uc.increment_progress(user_id, created.id, 3).await.unwrap();
1399 let result = uc.increment_progress(user_id, created.id, 3).await; 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}