koprogo_api/infrastructure/web/handlers/
skill_handlers.rs

1use crate::application::dto::{CreateSkillDto, UpdateSkillDto};
2use crate::domain::entities::{ExpertiseLevel, SkillCategory};
3use crate::infrastructure::web::app_state::AppState;
4use crate::infrastructure::web::middleware::AuthenticatedUser;
5use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
6use uuid::Uuid;
7
8/// Create a new skill
9///
10/// POST /skills
11#[post("/skills")]
12pub async fn create_skill(
13    data: web::Data<AppState>,
14    auth: AuthenticatedUser,
15    request: web::Json<CreateSkillDto>,
16) -> impl Responder {
17    let org_id = match auth.require_organization() {
18        Ok(id) => id,
19        Err(e) => {
20            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
21        }
22    };
23    match data
24        .skill_use_cases
25        .create_skill(auth.user_id, org_id, request.into_inner())
26        .await
27    {
28        Ok(skill) => HttpResponse::Created().json(skill),
29        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
30    }
31}
32
33/// Get skill by ID with owner name enrichment
34///
35/// GET /skills/:id
36#[get("/skills/{id}")]
37pub async fn get_skill(data: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
38    match data.skill_use_cases.get_skill(id.into_inner()).await {
39        Ok(skill) => HttpResponse::Ok().json(skill),
40        Err(e) => {
41            if e.contains("not found") {
42                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
43            } else {
44                HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
45            }
46        }
47    }
48}
49
50/// List all skills for a building
51///
52/// GET /buildings/:building_id/skills
53#[get("/buildings/{building_id}/skills")]
54pub async fn list_building_skills(
55    data: web::Data<AppState>,
56    building_id: web::Path<Uuid>,
57) -> impl Responder {
58    match data
59        .skill_use_cases
60        .list_building_skills(building_id.into_inner())
61        .await
62    {
63        Ok(skills) => HttpResponse::Ok().json(skills),
64        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
65    }
66}
67
68/// List available skills for a building (marketplace view)
69///
70/// GET /buildings/:building_id/skills/available
71#[get("/buildings/{building_id}/skills/available")]
72pub async fn list_available_skills(
73    data: web::Data<AppState>,
74    building_id: web::Path<Uuid>,
75) -> impl Responder {
76    match data
77        .skill_use_cases
78        .list_available_skills(building_id.into_inner())
79        .await
80    {
81        Ok(skills) => HttpResponse::Ok().json(skills),
82        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
83    }
84}
85
86/// List free/volunteer skills for a building
87///
88/// GET /buildings/:building_id/skills/free
89#[get("/buildings/{building_id}/skills/free")]
90pub async fn list_free_skills(
91    data: web::Data<AppState>,
92    building_id: web::Path<Uuid>,
93) -> impl Responder {
94    match data
95        .skill_use_cases
96        .list_free_skills(building_id.into_inner())
97        .await
98    {
99        Ok(skills) => HttpResponse::Ok().json(skills),
100        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
101    }
102}
103
104/// List professional skills for a building
105///
106/// GET /buildings/:building_id/skills/professional
107#[get("/buildings/{building_id}/skills/professional")]
108pub async fn list_professional_skills(
109    data: web::Data<AppState>,
110    building_id: web::Path<Uuid>,
111) -> impl Responder {
112    match data
113        .skill_use_cases
114        .list_professional_skills(building_id.into_inner())
115        .await
116    {
117        Ok(skills) => HttpResponse::Ok().json(skills),
118        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
119    }
120}
121
122/// List skills by category (HomeRepair, Languages, Technology, etc.)
123///
124/// GET /buildings/:building_id/skills/category/:category
125#[get("/buildings/{building_id}/skills/category/{category}")]
126pub async fn list_skills_by_category(
127    data: web::Data<AppState>,
128    path: web::Path<(Uuid, String)>,
129) -> impl Responder {
130    let (building_id, category_str) = path.into_inner();
131
132    // Parse skill category
133    let category = match serde_json::from_str::<SkillCategory>(&format!("\"{}\"", category_str)) {
134        Ok(c) => c,
135        Err(_) => {
136            return HttpResponse::BadRequest().json(serde_json::json!({
137                "error": format!("Invalid skill category: {}. Valid categories: HomeRepair, Languages, Technology, Education, Arts, Sports, Cooking, Gardening, Health, Legal, Financial, PetCare, Other", category_str)
138            }))
139        }
140    };
141
142    match data
143        .skill_use_cases
144        .list_skills_by_category(building_id, category)
145        .await
146    {
147        Ok(skills) => HttpResponse::Ok().json(skills),
148        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
149    }
150}
151
152/// List skills by expertise level (Beginner, Intermediate, Advanced, Expert)
153///
154/// GET /buildings/:building_id/skills/expertise/:level
155#[get("/buildings/{building_id}/skills/expertise/{level}")]
156pub async fn list_skills_by_expertise(
157    data: web::Data<AppState>,
158    path: web::Path<(Uuid, String)>,
159) -> impl Responder {
160    let (building_id, level_str) = path.into_inner();
161
162    // Parse expertise level
163    let level = match serde_json::from_str::<ExpertiseLevel>(&format!("\"{}\"", level_str)) {
164        Ok(l) => l,
165        Err(_) => {
166            return HttpResponse::BadRequest().json(serde_json::json!({
167                "error": format!("Invalid expertise level: {}. Valid levels: Beginner, Intermediate, Advanced, Expert", level_str)
168            }))
169        }
170    };
171
172    match data
173        .skill_use_cases
174        .list_skills_by_expertise(building_id, level)
175        .await
176    {
177        Ok(skills) => HttpResponse::Ok().json(skills),
178        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
179    }
180}
181
182/// List all skills created by an owner
183///
184/// GET /owners/:owner_id/skills
185#[get("/owners/{owner_id}/skills")]
186pub async fn list_owner_skills(
187    data: web::Data<AppState>,
188    owner_id: web::Path<Uuid>,
189) -> impl Responder {
190    match data
191        .skill_use_cases
192        .list_owner_skills(owner_id.into_inner())
193        .await
194    {
195        Ok(skills) => HttpResponse::Ok().json(skills),
196        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
197    }
198}
199
200/// Update a skill
201///
202/// PUT /skills/:id
203#[put("/skills/{id}")]
204pub async fn update_skill(
205    data: web::Data<AppState>,
206    auth: AuthenticatedUser,
207    id: web::Path<Uuid>,
208    request: web::Json<UpdateSkillDto>,
209) -> impl Responder {
210    let org_id = match auth.require_organization() {
211        Ok(id) => id,
212        Err(e) => {
213            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
214        }
215    };
216    match data
217        .skill_use_cases
218        .update_skill(id.into_inner(), auth.user_id, org_id, request.into_inner())
219        .await
220    {
221        Ok(skill) => HttpResponse::Ok().json(skill),
222        Err(e) => {
223            if e.contains("Unauthorized") {
224                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
225            } else if e.contains("not found") {
226                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
227            } else {
228                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
229            }
230        }
231    }
232}
233
234/// Mark skill as available for help
235///
236/// POST /skills/:id/mark-available
237#[post("/skills/{id}/mark-available")]
238pub async fn mark_skill_available(
239    data: web::Data<AppState>,
240    auth: AuthenticatedUser,
241    id: web::Path<Uuid>,
242) -> impl Responder {
243    let org_id = match auth.require_organization() {
244        Ok(id) => id,
245        Err(e) => {
246            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
247        }
248    };
249    match data
250        .skill_use_cases
251        .mark_skill_available(id.into_inner(), auth.user_id, org_id)
252        .await
253    {
254        Ok(skill) => HttpResponse::Ok().json(skill),
255        Err(e) => {
256            if e.contains("Unauthorized") {
257                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
258            } else if e.contains("not found") {
259                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
260            } else {
261                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
262            }
263        }
264    }
265}
266
267/// Mark skill as unavailable for help
268///
269/// POST /skills/:id/mark-unavailable
270#[post("/skills/{id}/mark-unavailable")]
271pub async fn mark_skill_unavailable(
272    data: web::Data<AppState>,
273    auth: AuthenticatedUser,
274    id: web::Path<Uuid>,
275) -> impl Responder {
276    let org_id = match auth.require_organization() {
277        Ok(id) => id,
278        Err(e) => {
279            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
280        }
281    };
282    match data
283        .skill_use_cases
284        .mark_skill_unavailable(id.into_inner(), auth.user_id, org_id)
285        .await
286    {
287        Ok(skill) => HttpResponse::Ok().json(skill),
288        Err(e) => {
289            if e.contains("Unauthorized") {
290                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
291            } else if e.contains("not found") {
292                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
293            } else {
294                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
295            }
296        }
297    }
298}
299
300/// Delete a skill
301///
302/// DELETE /skills/:id
303#[delete("/skills/{id}")]
304pub async fn delete_skill(
305    data: web::Data<AppState>,
306    auth: AuthenticatedUser,
307    id: web::Path<Uuid>,
308) -> impl Responder {
309    let org_id = match auth.require_organization() {
310        Ok(id) => id,
311        Err(e) => {
312            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
313        }
314    };
315    match data
316        .skill_use_cases
317        .delete_skill(id.into_inner(), auth.user_id, org_id)
318        .await
319    {
320        Ok(_) => HttpResponse::NoContent().finish(),
321        Err(e) => {
322            if e.contains("Unauthorized") {
323                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
324            } else if e.contains("not found") {
325                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
326            } else {
327                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
328            }
329        }
330    }
331}
332
333/// Get skill statistics for a building
334///
335/// GET /buildings/:building_id/skills/statistics
336#[get("/buildings/{building_id}/skills/statistics")]
337pub async fn get_skill_statistics(
338    data: web::Data<AppState>,
339    building_id: web::Path<Uuid>,
340) -> impl Responder {
341    match data
342        .skill_use_cases
343        .get_skill_statistics(building_id.into_inner())
344        .await
345    {
346        Ok(stats) => HttpResponse::Ok().json(stats),
347        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
348    }
349}