koprogo_api/infrastructure/web/handlers/
skill_handlers.rs1use crate::application::dto::{CreateSkillDto, UpdateSkillDto};
2use crate::domain::entities::{ExpertiseLevel, SkillCategory};
3use crate::infrastructure::web::app_state::AppState;
4use crate::infrastructure::web::classification_erreurs;
5use crate::infrastructure::web::middleware::AuthenticatedUser;
6use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
7use uuid::Uuid;
8
9#[post("/skills")]
13pub async fn create_skill(
14 data: web::Data<AppState>,
15 auth: AuthenticatedUser,
16 request: web::Json<CreateSkillDto>,
17) -> impl Responder {
18 let org_id = match auth.require_organization() {
19 Ok(id) => id,
20 Err(e) => {
21 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
22 }
23 };
24 match data
25 .skill_use_cases
26 .create_skill(auth.user_id, org_id, request.into_inner())
27 .await
28 {
29 Ok(skill) => HttpResponse::Created().json(skill),
30 Err(e) => {
31 if classification_erreurs::est_refus_owner_requis(&e) {
37 HttpResponse::Forbidden().json(serde_json::json!({
38 "error": e,
39 "kind": "owner_profile_required",
40 }))
41 } else if classification_erreurs::est_interdit(&e) {
42 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
43 } else if classification_erreurs::est_introuvable(&e) {
44 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
45 } else {
46 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
47 }
48 }
49 }
50}
51
52#[get("/skills/{id}")]
56pub async fn get_skill(
57 data: web::Data<AppState>,
58 user: AuthenticatedUser,
59 id: web::Path<Uuid>,
60) -> impl Responder {
61 let identifiant = id.into_inner();
62
63 if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_skill_org_access(
68 &user,
69 identifiant,
70 &data.skill_use_cases,
71 &data.building_use_cases,
72 &data.acp_use_cases,
73 )
74 .await
75 {
76 return err.error_response();
77 }
78
79 match data.skill_use_cases.get_skill(identifiant).await {
80 Ok(skill) => HttpResponse::Ok().json(skill),
81 Err(e) => {
82 if classification_erreurs::est_introuvable(&e) {
83 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
84 } else {
85 HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
86 }
87 }
88 }
89}
90
91#[get("/buildings/{building_id}/skills")]
95pub async fn list_building_skills(
96 data: web::Data<AppState>,
97 building_id: web::Path<Uuid>,
98 user: AuthenticatedUser,
99) -> impl Responder {
100 if let Err(err) =
104 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
105 &user,
106 *building_id,
107 &data.building_use_cases,
108 &data.acp_use_cases,
109 )
110 .await
111 {
112 return err.error_response();
113 }
114
115 match data
116 .skill_use_cases
117 .list_building_skills(building_id.into_inner())
118 .await
119 {
120 Ok(skills) => HttpResponse::Ok().json(skills),
121 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
122 }
123}
124
125#[get("/buildings/{building_id}/skills/available")]
129pub async fn list_available_skills(
130 data: web::Data<AppState>,
131 building_id: web::Path<Uuid>,
132 user: AuthenticatedUser,
133) -> impl Responder {
134 if let Err(err) =
138 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
139 &user,
140 *building_id,
141 &data.building_use_cases,
142 &data.acp_use_cases,
143 )
144 .await
145 {
146 return err.error_response();
147 }
148
149 match data
150 .skill_use_cases
151 .list_available_skills(building_id.into_inner())
152 .await
153 {
154 Ok(skills) => HttpResponse::Ok().json(skills),
155 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
156 }
157}
158
159#[get("/buildings/{building_id}/skills/free")]
163pub async fn list_free_skills(
164 data: web::Data<AppState>,
165 building_id: web::Path<Uuid>,
166 user: AuthenticatedUser,
167) -> impl Responder {
168 if let Err(err) =
172 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
173 &user,
174 *building_id,
175 &data.building_use_cases,
176 &data.acp_use_cases,
177 )
178 .await
179 {
180 return err.error_response();
181 }
182
183 match data
184 .skill_use_cases
185 .list_free_skills(building_id.into_inner())
186 .await
187 {
188 Ok(skills) => HttpResponse::Ok().json(skills),
189 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
190 }
191}
192
193#[get("/buildings/{building_id}/skills/professional")]
197pub async fn list_professional_skills(
198 data: web::Data<AppState>,
199 building_id: web::Path<Uuid>,
200 user: AuthenticatedUser,
201) -> impl Responder {
202 if let Err(err) =
206 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
207 &user,
208 *building_id,
209 &data.building_use_cases,
210 &data.acp_use_cases,
211 )
212 .await
213 {
214 return err.error_response();
215 }
216
217 match data
218 .skill_use_cases
219 .list_professional_skills(building_id.into_inner())
220 .await
221 {
222 Ok(skills) => HttpResponse::Ok().json(skills),
223 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
224 }
225}
226
227#[get("/buildings/{building_id}/skills/category/{category}")]
231pub async fn list_skills_by_category(
232 data: web::Data<AppState>,
233 path: web::Path<(Uuid, String)>,
234 user: AuthenticatedUser,
235) -> impl Responder {
236 let (building_id, category_str) = path.into_inner();
237 if let Err(err) =
241 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
242 &user,
243 building_id,
244 &data.building_use_cases,
245 &data.acp_use_cases,
246 )
247 .await
248 {
249 return err.error_response();
250 }
251
252 let category = match serde_json::from_str::<SkillCategory>(&format!("\"{}\"", category_str)) {
254 Ok(c) => c,
255 Err(_) => {
256 return HttpResponse::BadRequest().json(serde_json::json!({
257 "error": format!("Invalid skill category: {}. Valid categories: HomeRepair, Languages, Technology, Education, Arts, Sports, Cooking, Gardening, Health, Legal, Financial, PetCare, Other", category_str)
258 }))
259 }
260 };
261
262 match data
263 .skill_use_cases
264 .list_skills_by_category(building_id, category)
265 .await
266 {
267 Ok(skills) => HttpResponse::Ok().json(skills),
268 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
269 }
270}
271
272#[get("/buildings/{building_id}/skills/expertise/{level}")]
276pub async fn list_skills_by_expertise(
277 data: web::Data<AppState>,
278 path: web::Path<(Uuid, String)>,
279 user: AuthenticatedUser,
280) -> impl Responder {
281 let (building_id, level_str) = path.into_inner();
282 if let Err(err) =
286 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
287 &user,
288 building_id,
289 &data.building_use_cases,
290 &data.acp_use_cases,
291 )
292 .await
293 {
294 return err.error_response();
295 }
296
297 let level = match serde_json::from_str::<ExpertiseLevel>(&format!("\"{}\"", level_str)) {
299 Ok(l) => l,
300 Err(_) => {
301 return HttpResponse::BadRequest().json(serde_json::json!({
302 "error": format!("Invalid expertise level: {}. Valid levels: Beginner, Intermediate, Advanced, Expert", level_str)
303 }))
304 }
305 };
306
307 match data
308 .skill_use_cases
309 .list_skills_by_expertise(building_id, level)
310 .await
311 {
312 Ok(skills) => HttpResponse::Ok().json(skills),
313 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
314 }
315}
316
317#[get("/owners/{owner_id}/skills")]
321pub async fn list_owner_skills(
322 data: web::Data<AppState>,
323 owner_id: web::Path<Uuid>,
324 user: AuthenticatedUser,
325) -> impl Responder {
326 if let Err(err) = crate::infrastructure::web::middleware::scope_guard::verify_owner_org_access(
330 &user,
331 *owner_id,
332 &data.owner_use_cases,
333 )
334 .await
335 {
336 return err.error_response();
337 }
338
339 match data
340 .skill_use_cases
341 .list_owner_skills(owner_id.into_inner())
342 .await
343 {
344 Ok(skills) => HttpResponse::Ok().json(skills),
345 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
346 }
347}
348
349#[put("/skills/{id}")]
353pub async fn update_skill(
354 data: web::Data<AppState>,
355 auth: AuthenticatedUser,
356 id: web::Path<Uuid>,
357 request: web::Json<UpdateSkillDto>,
358) -> impl Responder {
359 let org_id = match auth.require_organization() {
360 Ok(id) => id,
361 Err(e) => {
362 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
363 }
364 };
365 match data
366 .skill_use_cases
367 .update_skill(id.into_inner(), auth.user_id, org_id, request.into_inner())
368 .await
369 {
370 Ok(skill) => HttpResponse::Ok().json(skill),
371 Err(e) => {
372 if classification_erreurs::est_interdit(&e) {
373 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
374 } else if classification_erreurs::est_introuvable(&e) {
375 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
376 } else {
377 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
378 }
379 }
380 }
381}
382
383#[post("/skills/{id}/mark-available")]
387pub async fn mark_skill_available(
388 data: web::Data<AppState>,
389 auth: AuthenticatedUser,
390 id: web::Path<Uuid>,
391) -> impl Responder {
392 let org_id = match auth.require_organization() {
393 Ok(id) => id,
394 Err(e) => {
395 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
396 }
397 };
398 match data
399 .skill_use_cases
400 .mark_skill_available(id.into_inner(), auth.user_id, org_id)
401 .await
402 {
403 Ok(skill) => HttpResponse::Ok().json(skill),
404 Err(e) => {
405 if classification_erreurs::est_interdit(&e) {
406 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
407 } else if classification_erreurs::est_introuvable(&e) {
408 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
409 } else {
410 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
411 }
412 }
413 }
414}
415
416#[post("/skills/{id}/mark-unavailable")]
420pub async fn mark_skill_unavailable(
421 data: web::Data<AppState>,
422 auth: AuthenticatedUser,
423 id: web::Path<Uuid>,
424) -> impl Responder {
425 let org_id = match auth.require_organization() {
426 Ok(id) => id,
427 Err(e) => {
428 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
429 }
430 };
431 match data
432 .skill_use_cases
433 .mark_skill_unavailable(id.into_inner(), auth.user_id, org_id)
434 .await
435 {
436 Ok(skill) => HttpResponse::Ok().json(skill),
437 Err(e) => {
438 if classification_erreurs::est_interdit(&e) {
439 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
440 } else if classification_erreurs::est_introuvable(&e) {
441 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
442 } else {
443 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
444 }
445 }
446 }
447}
448
449#[delete("/skills/{id}")]
453pub async fn delete_skill(
454 data: web::Data<AppState>,
455 auth: AuthenticatedUser,
456 id: web::Path<Uuid>,
457) -> impl Responder {
458 let org_id = match auth.require_organization() {
459 Ok(id) => id,
460 Err(e) => {
461 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
462 }
463 };
464 match data
465 .skill_use_cases
466 .delete_skill(id.into_inner(), auth.user_id, org_id)
467 .await
468 {
469 Ok(_) => HttpResponse::NoContent().finish(),
470 Err(e) => {
471 if classification_erreurs::est_interdit(&e) {
472 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
473 } else if classification_erreurs::est_introuvable(&e) {
474 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
475 } else {
476 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
477 }
478 }
479 }
480}
481
482#[get("/buildings/{building_id}/skills/statistics")]
486pub async fn get_skill_statistics(
487 data: web::Data<AppState>,
488 building_id: web::Path<Uuid>,
489 user: AuthenticatedUser,
490) -> impl Responder {
491 if let Err(err) =
495 crate::infrastructure::web::middleware::scope_guard::verify_building_org_access(
496 &user,
497 *building_id,
498 &data.building_use_cases,
499 &data.acp_use_cases,
500 )
501 .await
502 {
503 return err.error_response();
504 }
505
506 match data
507 .skill_use_cases
508 .get_skill_statistics(building_id.into_inner())
509 .await
510 {
511 Ok(stats) => HttpResponse::Ok().json(stats),
512 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
513 }
514}