1use crate::application::dto::{
2 CategoryCount, CreateSkillDto, ExpertiseCount, SkillResponseDto, SkillStatisticsDto,
3 SkillSummaryDto, UpdateSkillDto,
4};
5use crate::application::ports::{OwnerRepository, SkillRepository};
6use crate::domain::entities::{ExpertiseLevel, Skill, SkillCategory};
7use std::sync::Arc;
8use uuid::Uuid;
9
10pub struct SkillUseCases {
11 skill_repo: Arc<dyn SkillRepository>,
12 owner_repo: Arc<dyn OwnerRepository>,
13}
14
15impl SkillUseCases {
16 pub fn new(skill_repo: Arc<dyn SkillRepository>, owner_repo: Arc<dyn OwnerRepository>) -> Self {
17 Self {
18 skill_repo,
19 owner_repo,
20 }
21 }
22
23 async fn resolve_owner(
25 &self,
26 user_id: Uuid,
27 organization_id: Uuid,
28 ) -> Result<crate::domain::entities::Owner, String> {
29 self.owner_repo
30 .find_by_user_id_and_organization(user_id, organization_id)
31 .await?
32 .ok_or_else(|| "Owner not found for this user in the organization".to_string())
33 }
34
35 pub async fn create_skill(
40 &self,
41 user_id: Uuid,
42 organization_id: Uuid,
43 dto: CreateSkillDto,
44 ) -> Result<SkillResponseDto, String> {
45 let owner = self.resolve_owner(user_id, organization_id).await?;
47 let owner_id = owner.id;
48
49 let skill = Skill::new(
51 owner_id,
52 dto.building_id,
53 dto.skill_category,
54 dto.skill_name,
55 dto.expertise_level,
56 dto.description,
57 dto.is_available_for_help,
58 dto.hourly_rate_credits,
59 dto.years_of_experience,
60 dto.certifications,
61 )?;
62
63 let created = self.skill_repo.create(&skill).await?;
65
66 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
68 Ok(SkillResponseDto::from_skill(created, owner_name))
69 }
70
71 pub async fn get_skill(&self, skill_id: Uuid) -> Result<SkillResponseDto, String> {
73 let skill = self
74 .skill_repo
75 .find_by_id(skill_id)
76 .await?
77 .ok_or("Skill not found".to_string())?;
78
79 let owner = self
81 .owner_repo
82 .find_by_id(skill.owner_id)
83 .await?
84 .ok_or("Owner not found".to_string())?;
85
86 let owner_name = format!("{} {}", owner.first_name, owner.last_name);
87 Ok(SkillResponseDto::from_skill(skill, owner_name))
88 }
89
90 pub async fn list_building_skills(
95 &self,
96 building_id: Uuid,
97 ) -> Result<Vec<SkillSummaryDto>, String> {
98 let skills = self.skill_repo.find_by_building(building_id).await?;
99 self.enrich_skills_summary(skills).await
100 }
101
102 pub async fn list_available_skills(
107 &self,
108 building_id: Uuid,
109 ) -> Result<Vec<SkillSummaryDto>, String> {
110 let skills = self
111 .skill_repo
112 .find_available_by_building(building_id)
113 .await?;
114 self.enrich_skills_summary(skills).await
115 }
116
117 pub async fn list_owner_skills(&self, owner_id: Uuid) -> Result<Vec<SkillSummaryDto>, String> {
119 let skills = self.skill_repo.find_by_owner(owner_id).await?;
120 self.enrich_skills_summary(skills).await
121 }
122
123 pub async fn list_skills_by_category(
125 &self,
126 building_id: Uuid,
127 category: SkillCategory,
128 ) -> Result<Vec<SkillSummaryDto>, String> {
129 let skills = self
130 .skill_repo
131 .find_by_category(building_id, category)
132 .await?;
133 self.enrich_skills_summary(skills).await
134 }
135
136 pub async fn list_skills_by_expertise(
138 &self,
139 building_id: Uuid,
140 level: ExpertiseLevel,
141 ) -> Result<Vec<SkillSummaryDto>, String> {
142 let skills = self
143 .skill_repo
144 .find_by_expertise(building_id, level)
145 .await?;
146 self.enrich_skills_summary(skills).await
147 }
148
149 pub async fn list_free_skills(
151 &self,
152 building_id: Uuid,
153 ) -> Result<Vec<SkillSummaryDto>, String> {
154 let skills = self.skill_repo.find_free_by_building(building_id).await?;
155 self.enrich_skills_summary(skills).await
156 }
157
158 pub async fn list_professional_skills(
160 &self,
161 building_id: Uuid,
162 ) -> Result<Vec<SkillSummaryDto>, String> {
163 let skills = self
164 .skill_repo
165 .find_professional_by_building(building_id)
166 .await?;
167 self.enrich_skills_summary(skills).await
168 }
169
170 pub async fn update_skill(
175 &self,
176 skill_id: Uuid,
177 user_id: Uuid,
178 organization_id: Uuid,
179 dto: UpdateSkillDto,
180 ) -> Result<SkillResponseDto, String> {
181 let owner = self.resolve_owner(user_id, organization_id).await?;
182 let mut skill = self
183 .skill_repo
184 .find_by_id(skill_id)
185 .await?
186 .ok_or("Skill not found".to_string())?;
187
188 if skill.owner_id != owner.id {
190 return Err("Unauthorized: only owner can update skill".to_string());
191 }
192
193 skill.update(
195 dto.skill_name,
196 dto.expertise_level,
197 dto.description,
198 dto.is_available_for_help,
199 dto.hourly_rate_credits,
200 dto.years_of_experience,
201 dto.certifications,
202 )?;
203
204 let updated = self.skill_repo.update(&skill).await?;
206
207 self.get_skill(updated.id).await
209 }
210
211 pub async fn mark_skill_available(
216 &self,
217 skill_id: Uuid,
218 user_id: Uuid,
219 organization_id: Uuid,
220 ) -> Result<SkillResponseDto, String> {
221 let owner = self.resolve_owner(user_id, organization_id).await?;
222 let mut skill = self
223 .skill_repo
224 .find_by_id(skill_id)
225 .await?
226 .ok_or("Skill not found".to_string())?;
227
228 if skill.owner_id != owner.id {
230 return Err("Unauthorized: only owner can mark skill as available".to_string());
231 }
232
233 skill.mark_available();
235
236 let updated = self.skill_repo.update(&skill).await?;
238
239 self.get_skill(updated.id).await
241 }
242
243 pub async fn mark_skill_unavailable(
248 &self,
249 skill_id: Uuid,
250 user_id: Uuid,
251 organization_id: Uuid,
252 ) -> Result<SkillResponseDto, String> {
253 let owner = self.resolve_owner(user_id, organization_id).await?;
254 let mut skill = self
255 .skill_repo
256 .find_by_id(skill_id)
257 .await?
258 .ok_or("Skill not found".to_string())?;
259
260 if skill.owner_id != owner.id {
262 return Err("Unauthorized: only owner can mark skill as unavailable".to_string());
263 }
264
265 skill.mark_unavailable();
267
268 let updated = self.skill_repo.update(&skill).await?;
270
271 self.get_skill(updated.id).await
273 }
274
275 pub async fn delete_skill(
280 &self,
281 skill_id: Uuid,
282 user_id: Uuid,
283 organization_id: Uuid,
284 ) -> Result<(), String> {
285 let owner = self.resolve_owner(user_id, organization_id).await?;
286 let skill = self
287 .skill_repo
288 .find_by_id(skill_id)
289 .await?
290 .ok_or("Skill not found".to_string())?;
291
292 if skill.owner_id != owner.id {
294 return Err("Unauthorized: only owner can delete skill".to_string());
295 }
296
297 self.skill_repo.delete(skill_id).await?;
299
300 Ok(())
301 }
302
303 pub async fn get_skill_statistics(
305 &self,
306 building_id: Uuid,
307 ) -> Result<SkillStatisticsDto, String> {
308 let total_skills = self.skill_repo.count_by_building(building_id).await?;
309 let available_skills = self
310 .skill_repo
311 .count_available_by_building(building_id)
312 .await?;
313
314 let skills = self.skill_repo.find_by_building(building_id).await?;
316 let free_skills = skills.iter().filter(|s| s.is_free()).count() as i64;
317 let paid_skills = total_skills - free_skills;
318 let professional_skills = skills.iter().filter(|s| s.is_professional()).count() as i64;
319
320 let mut skills_by_category = Vec::new();
322 for category in [
323 SkillCategory::HomeRepair,
324 SkillCategory::Languages,
325 SkillCategory::Technology,
326 SkillCategory::Education,
327 SkillCategory::Arts,
328 SkillCategory::Sports,
329 SkillCategory::Cooking,
330 SkillCategory::Gardening,
331 SkillCategory::Health,
332 SkillCategory::Legal,
333 SkillCategory::Financial,
334 SkillCategory::PetCare,
335 SkillCategory::Other,
336 ] {
337 let count = self
338 .skill_repo
339 .count_by_category(building_id, category.clone())
340 .await?;
341 if count > 0 {
342 skills_by_category.push(CategoryCount { category, count });
343 }
344 }
345
346 let mut skills_by_expertise = Vec::new();
348 for level in [
349 ExpertiseLevel::Beginner,
350 ExpertiseLevel::Intermediate,
351 ExpertiseLevel::Advanced,
352 ExpertiseLevel::Expert,
353 ] {
354 let count = self
355 .skill_repo
356 .count_by_expertise(building_id, level.clone())
357 .await?;
358 if count > 0 {
359 skills_by_expertise.push(ExpertiseCount { level, count });
360 }
361 }
362
363 Ok(SkillStatisticsDto {
364 total_skills,
365 available_skills,
366 free_skills,
367 paid_skills,
368 professional_skills,
369 skills_by_category,
370 skills_by_expertise,
371 })
372 }
373
374 async fn enrich_skills_summary(
376 &self,
377 skills: Vec<Skill>,
378 ) -> Result<Vec<SkillSummaryDto>, String> {
379 let mut enriched = Vec::new();
380
381 for skill in skills {
382 let owner = self.owner_repo.find_by_id(skill.owner_id).await?;
384 let owner_name = if let Some(owner) = owner {
385 format!("{} {}", owner.first_name, owner.last_name)
386 } else {
387 "Unknown Owner".to_string()
388 };
389
390 enriched.push(SkillSummaryDto::from_skill(skill, owner_name));
391 }
392
393 Ok(enriched)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::application::dto::{OwnerFilters, PageRequest};
401 use crate::application::ports::{OwnerRepository, SkillRepository};
402 use crate::domain::entities::{ExpertiseLevel, Owner, Skill, SkillCategory};
403 use async_trait::async_trait;
404 use std::collections::HashMap;
405 use std::sync::{Arc, Mutex};
406 use uuid::Uuid;
407
408 struct MockSkillRepo {
410 skills: Mutex<HashMap<Uuid, Skill>>,
411 }
412
413 impl MockSkillRepo {
414 fn new() -> Self {
415 Self {
416 skills: Mutex::new(HashMap::new()),
417 }
418 }
419 }
420
421 #[async_trait]
422 impl SkillRepository for MockSkillRepo {
423 async fn create(&self, skill: &Skill) -> Result<Skill, String> {
424 let mut map = self.skills.lock().unwrap();
425 map.insert(skill.id, skill.clone());
426 Ok(skill.clone())
427 }
428
429 async fn find_by_id(&self, id: Uuid) -> Result<Option<Skill>, String> {
430 Ok(self.skills.lock().unwrap().get(&id).cloned())
431 }
432
433 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Skill>, String> {
434 Ok(self
435 .skills
436 .lock()
437 .unwrap()
438 .values()
439 .filter(|s| s.building_id == building_id)
440 .cloned()
441 .collect())
442 }
443
444 async fn find_available_by_building(
445 &self,
446 building_id: Uuid,
447 ) -> Result<Vec<Skill>, String> {
448 Ok(self
449 .skills
450 .lock()
451 .unwrap()
452 .values()
453 .filter(|s| s.building_id == building_id && s.is_available_for_help)
454 .cloned()
455 .collect())
456 }
457
458 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Skill>, String> {
459 Ok(self
460 .skills
461 .lock()
462 .unwrap()
463 .values()
464 .filter(|s| s.owner_id == owner_id)
465 .cloned()
466 .collect())
467 }
468
469 async fn find_by_category(
470 &self,
471 building_id: Uuid,
472 category: SkillCategory,
473 ) -> Result<Vec<Skill>, String> {
474 Ok(self
475 .skills
476 .lock()
477 .unwrap()
478 .values()
479 .filter(|s| s.building_id == building_id && s.skill_category == category)
480 .cloned()
481 .collect())
482 }
483
484 async fn find_by_expertise(
485 &self,
486 building_id: Uuid,
487 level: ExpertiseLevel,
488 ) -> Result<Vec<Skill>, String> {
489 Ok(self
490 .skills
491 .lock()
492 .unwrap()
493 .values()
494 .filter(|s| s.building_id == building_id && s.expertise_level == level)
495 .cloned()
496 .collect())
497 }
498
499 async fn find_free_by_building(&self, building_id: Uuid) -> Result<Vec<Skill>, String> {
500 Ok(self
501 .skills
502 .lock()
503 .unwrap()
504 .values()
505 .filter(|s| s.building_id == building_id && s.is_free())
506 .cloned()
507 .collect())
508 }
509
510 async fn find_professional_by_building(
511 &self,
512 building_id: Uuid,
513 ) -> Result<Vec<Skill>, String> {
514 Ok(self
515 .skills
516 .lock()
517 .unwrap()
518 .values()
519 .filter(|s| s.building_id == building_id && s.is_professional())
520 .cloned()
521 .collect())
522 }
523
524 async fn update(&self, skill: &Skill) -> Result<Skill, String> {
525 let mut map = self.skills.lock().unwrap();
526 map.insert(skill.id, skill.clone());
527 Ok(skill.clone())
528 }
529
530 async fn delete(&self, id: Uuid) -> Result<(), String> {
531 self.skills.lock().unwrap().remove(&id);
532 Ok(())
533 }
534
535 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
536 Ok(self
537 .skills
538 .lock()
539 .unwrap()
540 .values()
541 .filter(|s| s.building_id == building_id)
542 .count() as i64)
543 }
544
545 async fn count_available_by_building(&self, building_id: Uuid) -> Result<i64, String> {
546 Ok(self
547 .skills
548 .lock()
549 .unwrap()
550 .values()
551 .filter(|s| s.building_id == building_id && s.is_available_for_help)
552 .count() as i64)
553 }
554
555 async fn count_by_category(
556 &self,
557 building_id: Uuid,
558 category: SkillCategory,
559 ) -> Result<i64, String> {
560 Ok(self
561 .skills
562 .lock()
563 .unwrap()
564 .values()
565 .filter(|s| s.building_id == building_id && s.skill_category == category)
566 .count() as i64)
567 }
568
569 async fn count_by_expertise(
570 &self,
571 building_id: Uuid,
572 level: ExpertiseLevel,
573 ) -> Result<i64, String> {
574 Ok(self
575 .skills
576 .lock()
577 .unwrap()
578 .values()
579 .filter(|s| s.building_id == building_id && s.expertise_level == level)
580 .count() as i64)
581 }
582 }
583
584 struct MockOwnerRepo {
586 owners: Mutex<HashMap<Uuid, Owner>>,
587 }
588
589 impl MockOwnerRepo {
590 fn new() -> Self {
591 Self {
592 owners: Mutex::new(HashMap::new()),
593 }
594 }
595 fn add_owner(&self, owner: Owner) {
596 self.owners.lock().unwrap().insert(owner.id, owner);
597 }
598 }
599
600 #[async_trait]
601 impl OwnerRepository for MockOwnerRepo {
602 async fn create(&self, owner: &Owner) -> Result<Owner, String> {
603 self.owners.lock().unwrap().insert(owner.id, owner.clone());
604 Ok(owner.clone())
605 }
606 async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
607 Ok(self.owners.lock().unwrap().get(&id).cloned())
608 }
609 async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
610 Ok(self
611 .owners
612 .lock()
613 .unwrap()
614 .values()
615 .find(|o| o.user_id == Some(user_id))
616 .cloned())
617 }
618 async fn find_by_user_id_and_organization(
619 &self,
620 user_id: Uuid,
621 org_id: Uuid,
622 ) -> Result<Option<Owner>, String> {
623 Ok(self
624 .owners
625 .lock()
626 .unwrap()
627 .values()
628 .find(|o| o.user_id == Some(user_id) && o.organization_id == org_id)
629 .cloned())
630 }
631 async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
632 Ok(self
633 .owners
634 .lock()
635 .unwrap()
636 .values()
637 .find(|o| o.email == email)
638 .cloned())
639 }
640 async fn find_all(&self) -> Result<Vec<Owner>, String> {
641 Ok(self.owners.lock().unwrap().values().cloned().collect())
642 }
643 async fn find_all_paginated(
644 &self,
645 _p: &PageRequest,
646 _f: &OwnerFilters,
647 ) -> Result<(Vec<Owner>, i64), String> {
648 let v: Vec<_> = self.owners.lock().unwrap().values().cloned().collect();
649 let c = v.len() as i64;
650 Ok((v, c))
651 }
652 async fn update(&self, owner: &Owner) -> Result<Owner, String> {
653 self.owners.lock().unwrap().insert(owner.id, owner.clone());
654 Ok(owner.clone())
655 }
656 async fn delete(&self, id: Uuid) -> Result<bool, String> {
657 Ok(self.owners.lock().unwrap().remove(&id).is_some())
658 }
659 }
660
661 fn create_test_owner(user_id: Uuid, org_id: Uuid) -> Owner {
663 let mut owner = Owner::new(
664 org_id,
665 "Marie".to_string(),
666 "Lefevre".to_string(),
667 "marie@test.com".to_string(),
668 None,
669 "Rue Haute 5".to_string(),
670 "Brussels".to_string(),
671 "1000".to_string(),
672 "Belgium".to_string(),
673 )
674 .unwrap();
675 owner.user_id = Some(user_id);
676 owner
677 }
678
679 fn setup() -> (SkillUseCases, Uuid, Uuid, Uuid) {
680 let user_id = Uuid::new_v4();
681 let org_id = Uuid::new_v4();
682 let building_id = Uuid::new_v4();
683
684 let skill_repo = Arc::new(MockSkillRepo::new());
685 let owner_repo = Arc::new(MockOwnerRepo::new());
686
687 let owner = create_test_owner(user_id, org_id);
688 owner_repo.add_owner(owner);
689
690 let uc = SkillUseCases::new(
691 skill_repo as Arc<dyn SkillRepository>,
692 owner_repo as Arc<dyn OwnerRepository>,
693 );
694
695 (uc, user_id, org_id, building_id)
696 }
697
698 fn make_create_dto(building_id: Uuid) -> CreateSkillDto {
699 CreateSkillDto {
700 building_id,
701 skill_category: SkillCategory::Technology,
702 skill_name: "Web Development".to_string(),
703 expertise_level: ExpertiseLevel::Advanced,
704 description: "Full-stack web development with React and Node".to_string(),
705 is_available_for_help: true,
706 hourly_rate_credits: Some(10),
707 years_of_experience: Some(5),
708 certifications: Some("AWS Certified".to_string()),
709 }
710 }
711
712 #[tokio::test]
715 async fn test_create_skill_success() {
716 let (uc, user_id, org_id, building_id) = setup();
717 let dto = make_create_dto(building_id);
718 let result = uc.create_skill(user_id, org_id, dto).await;
719 assert!(result.is_ok());
720 let resp = result.unwrap();
721 assert_eq!(resp.skill_name, "Web Development");
722 assert_eq!(resp.owner_name, "Marie Lefevre");
723 }
724
725 #[tokio::test]
726 async fn test_get_skill_success() {
727 let (uc, user_id, org_id, building_id) = setup();
728 let dto = make_create_dto(building_id);
729 let created = uc.create_skill(user_id, org_id, dto).await.unwrap();
730
731 let result = uc.get_skill(created.id).await;
732 assert!(result.is_ok());
733 assert_eq!(result.unwrap().id, created.id);
734 }
735
736 #[tokio::test]
737 async fn test_get_skill_not_found() {
738 let (uc, _, _, _) = setup();
739 let result = uc.get_skill(Uuid::new_v4()).await;
740 assert!(result.is_err());
741 assert_eq!(result.unwrap_err(), "Skill not found");
742 }
743
744 #[tokio::test]
745 async fn test_delete_skill_success() {
746 let (uc, user_id, org_id, building_id) = setup();
747 let dto = make_create_dto(building_id);
748 let created = uc.create_skill(user_id, org_id, dto).await.unwrap();
749
750 let result = uc.delete_skill(created.id, user_id, org_id).await;
751 assert!(result.is_ok());
752 }
753
754 #[tokio::test]
755 async fn test_delete_skill_wrong_owner() {
756 let (uc, user_id, org_id, building_id) = setup();
757 let dto = make_create_dto(building_id);
758 let created = uc.create_skill(user_id, org_id, dto).await.unwrap();
759
760 let other = Uuid::new_v4();
762 let result = uc.delete_skill(created.id, other, org_id).await;
763 assert!(result.is_err());
764 assert!(result.unwrap_err().contains("Owner not found"));
765 }
766
767 #[tokio::test]
768 async fn test_list_building_skills() {
769 let (uc, user_id, org_id, building_id) = setup();
770
771 let dto1 = make_create_dto(building_id);
772 let mut dto2 = make_create_dto(building_id);
773 dto2.skill_name = "Plumbing".to_string();
774 dto2.skill_category = SkillCategory::HomeRepair;
775 dto2.description = "Residential plumbing repair and installation".to_string();
776
777 uc.create_skill(user_id, org_id, dto1).await.unwrap();
778 uc.create_skill(user_id, org_id, dto2).await.unwrap();
779
780 let result = uc.list_building_skills(building_id).await;
781 assert!(result.is_ok());
782 assert_eq!(result.unwrap().len(), 2);
783 }
784
785 #[tokio::test]
786 async fn test_owner_not_found() {
787 let skill_repo = Arc::new(MockSkillRepo::new());
788 let owner_repo = Arc::new(MockOwnerRepo::new());
789 let uc = SkillUseCases::new(
792 skill_repo as Arc<dyn SkillRepository>,
793 owner_repo as Arc<dyn OwnerRepository>,
794 );
795
796 let dto = make_create_dto(Uuid::new_v4());
797 let result = uc.create_skill(Uuid::new_v4(), Uuid::new_v4(), dto).await;
798 assert!(result.is_err());
799 assert!(result.unwrap_err().contains("Owner not found"));
800 }
801}