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(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 pub async fn delete_achievement(&self, achievement_id: Uuid) -> Result<(), String> {
176 self.achievement_repo.delete(achievement_id).await
177 }
178
179 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 let achievement = self
191 .achievement_repo
192 .find_by_id(achievement_id)
193 .await?
194 .ok_or("Achievement not found".to_string())?;
195
196 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 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 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 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 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 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 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
266pub 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 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 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 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 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 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 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 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 if challenge.status != ChallengeStatus::Draft {
400 return Err("Can only update Draft challenges".to_string());
401 }
402
403 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 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 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 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 pub async fn delete_challenge(&self, challenge_id: Uuid) -> Result<(), String> {
489 self.challenge_repo.delete(challenge_id).await
490 }
491
492 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 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 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 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 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 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 progress.increment(increment)?;
589
590 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
602pub 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 pub async fn get_user_stats(
637 &self,
638 user_id: Uuid,
639 organization_id: Uuid,
640 ) -> Result<UserGamificationStatsDto, String> {
641 let total_points = self
643 .user_achievement_repo
644 .calculate_total_points(user_id)
645 .await?;
646
647 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 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 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 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 pub async fn get_leaderboard(
695 &self,
696 organization_id: Uuid,
697 building_id: Option<Uuid>,
698 limit: i64,
699 ) -> Result<LeaderboardResponseDto, String> {
700 let leaderboard_data = self
702 .progress_repo
703 .get_leaderboard(organization_id, building_id, limit)
704 .await?;
705
706 let mut entries = Vec::new();
708 let mut rank = 1;
709 for (user_id, challenge_points) in leaderboard_data {
710 let achievement_points = self
712 .user_achievement_repo
713 .calculate_total_points(user_id)
714 .await?;
715
716 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 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}