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