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    /// Create a new skill
24    ///
25    /// # Authorization
26    /// - Owner must exist in the system
27    pub async fn create_skill(
28        &self,
29        owner_id: Uuid,
30        dto: CreateSkillDto,
31    ) -> Result<SkillResponseDto, String> {
32        // Verify owner exists
33        let owner = self
34            .owner_repo
35            .find_by_id(owner_id)
36            .await?
37            .ok_or("Owner not found".to_string())?;
38
39        // Create skill entity (validates business rules)
40        let skill = Skill::new(
41            owner_id,
42            dto.building_id,
43            dto.skill_category,
44            dto.skill_name,
45            dto.expertise_level,
46            dto.description,
47            dto.is_available_for_help,
48            dto.hourly_rate_credits,
49            dto.years_of_experience,
50            dto.certifications,
51        )?;
52
53        // Persist skill
54        let created = self.skill_repo.create(&skill).await?;
55
56        // Return enriched response
57        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
58        Ok(SkillResponseDto::from_skill(created, owner_name))
59    }
60
61    /// Get skill by ID with owner name enrichment
62    pub async fn get_skill(&self, skill_id: Uuid) -> Result<SkillResponseDto, String> {
63        let skill = self
64            .skill_repo
65            .find_by_id(skill_id)
66            .await?
67            .ok_or("Skill not found".to_string())?;
68
69        // Enrich with owner name
70        let owner = self
71            .owner_repo
72            .find_by_id(skill.owner_id)
73            .await?
74            .ok_or("Owner not found".to_string())?;
75
76        let owner_name = format!("{} {}", owner.first_name, owner.last_name);
77        Ok(SkillResponseDto::from_skill(skill, owner_name))
78    }
79
80    /// List all skills for a building
81    ///
82    /// # Returns
83    /// - Skills sorted by available (DESC), expertise (DESC), skill_name (ASC)
84    pub async fn list_building_skills(
85        &self,
86        building_id: Uuid,
87    ) -> Result<Vec<SkillSummaryDto>, String> {
88        let skills = self.skill_repo.find_by_building(building_id).await?;
89        self.enrich_skills_summary(skills).await
90    }
91
92    /// List available skills for a building (marketplace view)
93    ///
94    /// # Returns
95    /// - Only available skills (is_available_for_help = true)
96    pub async fn list_available_skills(
97        &self,
98        building_id: Uuid,
99    ) -> Result<Vec<SkillSummaryDto>, String> {
100        let skills = self
101            .skill_repo
102            .find_available_by_building(building_id)
103            .await?;
104        self.enrich_skills_summary(skills).await
105    }
106
107    /// List all skills created by an owner
108    pub async fn list_owner_skills(&self, owner_id: Uuid) -> Result<Vec<SkillSummaryDto>, String> {
109        let skills = self.skill_repo.find_by_owner(owner_id).await?;
110        self.enrich_skills_summary(skills).await
111    }
112
113    /// List skills by category (HomeRepair, Languages, Technology, etc.)
114    pub async fn list_skills_by_category(
115        &self,
116        building_id: Uuid,
117        category: SkillCategory,
118    ) -> Result<Vec<SkillSummaryDto>, String> {
119        let skills = self
120            .skill_repo
121            .find_by_category(building_id, category)
122            .await?;
123        self.enrich_skills_summary(skills).await
124    }
125
126    /// List skills by expertise level (Beginner, Intermediate, Advanced, Expert)
127    pub async fn list_skills_by_expertise(
128        &self,
129        building_id: Uuid,
130        level: ExpertiseLevel,
131    ) -> Result<Vec<SkillSummaryDto>, String> {
132        let skills = self
133            .skill_repo
134            .find_by_expertise(building_id, level)
135            .await?;
136        self.enrich_skills_summary(skills).await
137    }
138
139    /// List free/volunteer skills for a building
140    pub async fn list_free_skills(
141        &self,
142        building_id: Uuid,
143    ) -> Result<Vec<SkillSummaryDto>, String> {
144        let skills = self.skill_repo.find_free_by_building(building_id).await?;
145        self.enrich_skills_summary(skills).await
146    }
147
148    /// List professional skills for a building (Expert level OR certifications)
149    pub async fn list_professional_skills(
150        &self,
151        building_id: Uuid,
152    ) -> Result<Vec<SkillSummaryDto>, String> {
153        let skills = self
154            .skill_repo
155            .find_professional_by_building(building_id)
156            .await?;
157        self.enrich_skills_summary(skills).await
158    }
159
160    /// Update a skill
161    ///
162    /// # Authorization
163    /// - Only owner can update their skill
164    pub async fn update_skill(
165        &self,
166        skill_id: Uuid,
167        actor_id: Uuid,
168        dto: UpdateSkillDto,
169    ) -> Result<SkillResponseDto, String> {
170        let mut skill = self
171            .skill_repo
172            .find_by_id(skill_id)
173            .await?
174            .ok_or("Skill not found".to_string())?;
175
176        // Authorization: only owner can update
177        if skill.owner_id != actor_id {
178            return Err("Unauthorized: only owner can update skill".to_string());
179        }
180
181        // Update skill (domain validates business rules)
182        skill.update(
183            dto.skill_name,
184            dto.expertise_level,
185            dto.description,
186            dto.is_available_for_help,
187            dto.hourly_rate_credits,
188            dto.years_of_experience,
189            dto.certifications,
190        )?;
191
192        // Persist changes
193        let updated = self.skill_repo.update(&skill).await?;
194
195        // Return enriched response
196        self.get_skill(updated.id).await
197    }
198
199    /// Mark skill as available for help
200    ///
201    /// # Authorization
202    /// - Only owner can mark their skill as available
203    pub async fn mark_skill_available(
204        &self,
205        skill_id: Uuid,
206        actor_id: Uuid,
207    ) -> Result<SkillResponseDto, String> {
208        let mut skill = self
209            .skill_repo
210            .find_by_id(skill_id)
211            .await?
212            .ok_or("Skill not found".to_string())?;
213
214        // Authorization: only owner can mark available
215        if skill.owner_id != actor_id {
216            return Err("Unauthorized: only owner can mark skill as available".to_string());
217        }
218
219        // Mark available
220        skill.mark_available();
221
222        // Persist changes
223        let updated = self.skill_repo.update(&skill).await?;
224
225        // Return enriched response
226        self.get_skill(updated.id).await
227    }
228
229    /// Mark skill as unavailable for help
230    ///
231    /// # Authorization
232    /// - Only owner can mark their skill as unavailable
233    pub async fn mark_skill_unavailable(
234        &self,
235        skill_id: Uuid,
236        actor_id: Uuid,
237    ) -> Result<SkillResponseDto, String> {
238        let mut skill = self
239            .skill_repo
240            .find_by_id(skill_id)
241            .await?
242            .ok_or("Skill not found".to_string())?;
243
244        // Authorization: only owner can mark unavailable
245        if skill.owner_id != actor_id {
246            return Err("Unauthorized: only owner can mark skill as unavailable".to_string());
247        }
248
249        // Mark unavailable
250        skill.mark_unavailable();
251
252        // Persist changes
253        let updated = self.skill_repo.update(&skill).await?;
254
255        // Return enriched response
256        self.get_skill(updated.id).await
257    }
258
259    /// Delete a skill
260    ///
261    /// # Authorization
262    /// - Only owner can delete their skill
263    pub async fn delete_skill(&self, skill_id: Uuid, actor_id: Uuid) -> Result<(), String> {
264        let skill = self
265            .skill_repo
266            .find_by_id(skill_id)
267            .await?
268            .ok_or("Skill not found".to_string())?;
269
270        // Authorization: only owner can delete
271        if skill.owner_id != actor_id {
272            return Err("Unauthorized: only owner can delete skill".to_string());
273        }
274
275        // Delete skill
276        self.skill_repo.delete(skill_id).await?;
277
278        Ok(())
279    }
280
281    /// Get skill statistics for a building
282    pub async fn get_skill_statistics(
283        &self,
284        building_id: Uuid,
285    ) -> Result<SkillStatisticsDto, String> {
286        let total_skills = self.skill_repo.count_by_building(building_id).await?;
287        let available_skills = self
288            .skill_repo
289            .count_available_by_building(building_id)
290            .await?;
291
292        // Calculate free/paid skills
293        let skills = self.skill_repo.find_by_building(building_id).await?;
294        let free_skills = skills.iter().filter(|s| s.is_free()).count() as i64;
295        let paid_skills = total_skills - free_skills;
296        let professional_skills = skills.iter().filter(|s| s.is_professional()).count() as i64;
297
298        // Count by category
299        let mut skills_by_category = Vec::new();
300        for category in [
301            SkillCategory::HomeRepair,
302            SkillCategory::Languages,
303            SkillCategory::Technology,
304            SkillCategory::Education,
305            SkillCategory::Arts,
306            SkillCategory::Sports,
307            SkillCategory::Cooking,
308            SkillCategory::Gardening,
309            SkillCategory::Health,
310            SkillCategory::Legal,
311            SkillCategory::Financial,
312            SkillCategory::PetCare,
313            SkillCategory::Other,
314        ] {
315            let count = self
316                .skill_repo
317                .count_by_category(building_id, category.clone())
318                .await?;
319            if count > 0 {
320                skills_by_category.push(CategoryCount { category, count });
321            }
322        }
323
324        // Count by expertise level
325        let mut skills_by_expertise = Vec::new();
326        for level in [
327            ExpertiseLevel::Beginner,
328            ExpertiseLevel::Intermediate,
329            ExpertiseLevel::Advanced,
330            ExpertiseLevel::Expert,
331        ] {
332            let count = self
333                .skill_repo
334                .count_by_expertise(building_id, level.clone())
335                .await?;
336            if count > 0 {
337                skills_by_expertise.push(ExpertiseCount { level, count });
338            }
339        }
340
341        Ok(SkillStatisticsDto {
342            total_skills,
343            available_skills,
344            free_skills,
345            paid_skills,
346            professional_skills,
347            skills_by_category,
348            skills_by_expertise,
349        })
350    }
351
352    /// Helper method to enrich skills with owner names
353    async fn enrich_skills_summary(
354        &self,
355        skills: Vec<Skill>,
356    ) -> Result<Vec<SkillSummaryDto>, String> {
357        let mut enriched = Vec::new();
358
359        for skill in skills {
360            // Get owner name
361            let owner = self.owner_repo.find_by_id(skill.owner_id).await?;
362            let owner_name = if let Some(owner) = owner {
363                format!("{} {}", owner.first_name, owner.last_name)
364            } else {
365                "Unknown Owner".to_string()
366            };
367
368            enriched.push(SkillSummaryDto::from_skill(skill, owner_name));
369        }
370
371        Ok(enriched)
372    }
373}