Skip to main content

koprogo_api/application/use_cases/
skill_use_cases.rs

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    /// Resolve user_id to owner via organization lookup
24    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            // Refus LÉGITIME, message illisible.
33            //
34            // Le partage d'objets, l'offre de compétence et la réservation
35            // engagent une personne nommée : ils supposent donc une fiche de
36            // copropriétaire. Un syndic n'en a pas, et c'est normal.
37            //
38            // Ce qui ne l'était pas, c'est le message. « Owner not found for
39            // this user in the organization » est en anglais, technique, et ne
40            // dit pas à l'utilisateur ce qu'il devrait faire. En recette le
41            // 2026-09-06 (RN-11), il a été lu comme une panne bloquant quatre
42            // modules, alors que la création d'ANNONCE par un syndic fonctionne
43            // — parce qu'un avis, lui, émane de la copropriété.
44            //
45            // Le vrai manque est ailleurs : le syndic agissant pour le compte
46            // de l'ACP, prévu par la story #588 et non implémenté.
47            .ok_or_else(|| crate::application::error::REFUS_RESERVE_AUX_COPROPRIETAIRES.to_string())
48    }
49
50    /// Create a new skill
51    ///
52    /// # Authorization
53    /// - Owner must exist in the system
54    pub async fn create_skill(
55        &self,
56        user_id: Uuid,
57        organization_id: Uuid,
58        dto: CreateSkillDto,
59    ) -> Result<SkillResponseDto, String> {
60        // Resolve user_id to owner
61        let owner = self.resolve_owner(user_id, organization_id).await?;
62        let owner_id = owner.id;
63
64        // Create skill entity (validates business rules)
65        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        // Persist skill
79        let created = self.skill_repo.create(&skill).await?;
80
81        // Return enriched response
82        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
83        Ok(SkillResponseDto::from_skill(created, owner_name))
84    }
85
86    /// Get skill by ID with owner name enrichment
87    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        // Enrich with owner name
95        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    /// List all skills for a building
106    ///
107    /// # Returns
108    /// - Skills sorted by available (DESC), expertise (DESC), skill_name (ASC)
109    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    /// List available skills for a building (marketplace view)
118    ///
119    /// # Returns
120    /// - Only available skills (is_available_for_help = true)
121    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    /// List all skills created by an owner
133    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    /// List skills by category (HomeRepair, Languages, Technology, etc.)
139    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    /// List skills by expertise level (Beginner, Intermediate, Advanced, Expert)
152    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    /// List free/volunteer skills for a building
165    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    /// List professional skills for a building (Expert level OR certifications)
174    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    /// Update a skill
186    ///
187    /// # Authorization
188    /// - Only owner can update their skill
189    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        // Authorization: only owner can update
204        if skill.owner_id != owner.id {
205            return Err("Unauthorized: only owner can update skill".to_string());
206        }
207
208        // Update skill (domain validates business rules)
209        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        // Persist changes
220        let updated = self.skill_repo.update(&skill).await?;
221
222        // Return enriched response
223        self.get_skill(updated.id).await
224    }
225
226    /// Mark skill as available for help
227    ///
228    /// # Authorization
229    /// - Only owner can mark their skill as available
230    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        // Authorization: only owner can mark available
244        if skill.owner_id != owner.id {
245            return Err("Unauthorized: only owner can mark skill as available".to_string());
246        }
247
248        // Mark available
249        skill.mark_available();
250
251        // Persist changes
252        let updated = self.skill_repo.update(&skill).await?;
253
254        // Return enriched response
255        self.get_skill(updated.id).await
256    }
257
258    /// Mark skill as unavailable for help
259    ///
260    /// # Authorization
261    /// - Only owner can mark their skill as unavailable
262    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        // Authorization: only owner can mark unavailable
276        if skill.owner_id != owner.id {
277            return Err("Unauthorized: only owner can mark skill as unavailable".to_string());
278        }
279
280        // Mark unavailable
281        skill.mark_unavailable();
282
283        // Persist changes
284        let updated = self.skill_repo.update(&skill).await?;
285
286        // Return enriched response
287        self.get_skill(updated.id).await
288    }
289
290    /// Delete a skill
291    ///
292    /// # Authorization
293    /// - Only owner can delete their skill
294    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        // Authorization: only owner can delete
308        if skill.owner_id != owner.id {
309            return Err("Unauthorized: only owner can delete skill".to_string());
310        }
311
312        // Delete skill
313        self.skill_repo.delete(skill_id).await?;
314
315        Ok(())
316    }
317
318    /// Get skill statistics for a building
319    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        // Calculate free/paid skills
330        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        // Count by category
336        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        // Count by expertise level
362        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    /// Helper method to enrich skills with owner names
390    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            // Get owner name
398            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    // ── Mock SkillRepository ────────────────────────────────────────────────
424    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    // ── Mock OwnerRepository ────────────────────────────────────────────────
600    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    // ── Helpers ─────────────────────────────────────────────────────────────
690    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    // ── Tests ───────────────────────────────────────────────────────────────
741
742    #[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        // Unknown user
789        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        // No owner added
822
823        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}