1use crate::application::dto::{
2 CreateAchievementDto, CreateChallengeDto, UpdateAchievementDto, UpdateChallengeDto,
3};
4use crate::domain::entities::{AchievementCategory, ChallengeStatus};
5use crate::infrastructure::web::app_state::AppState;
6use crate::infrastructure::web::classification_erreurs;
7use crate::infrastructure::web::middleware::scope_guard::{
8 verify_building_org_access, verify_challenge_org_access,
9};
10use crate::infrastructure::web::middleware::AuthenticatedUser;
11use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
12use serde::Deserialize;
13use uuid::Uuid;
14
15#[post("/achievements")]
40pub async fn create_achievement(
41 data: web::Data<AppState>,
42 auth: AuthenticatedUser,
43 request: web::Json<CreateAchievementDto>,
44) -> impl Responder {
45 if !auth.is_superadmin() && auth.role != "syndic" {
46 return HttpResponse::Forbidden().json(serde_json::json!({
47 "error": "Only superadmin or syndic can create achievements"
48 }));
49 }
50 match data
51 .achievement_use_cases
52 .create_achievement(request.into_inner())
53 .await
54 {
55 Ok(achievement) => HttpResponse::Created().json(achievement),
56 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
57 }
58}
59
60#[get("/achievements/{id}")]
68pub async fn get_achievement(
69 data: web::Data<AppState>,
70 auth: AuthenticatedUser,
71 id: web::Path<Uuid>,
72) -> impl Responder {
73 match data
74 .achievement_use_cases
75 .get_achievement(id.into_inner())
76 .await
77 {
78 Ok(achievement) => {
79 if let Err(e) = auth.verify_org_access(achievement.organization_id) {
85 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
86 }
87 HttpResponse::Ok().json(achievement)
88 }
89 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
90 }
91}
92
93#[get("/organizations/{organization_id}/achievements")]
100pub async fn list_achievements(
101 data: web::Data<AppState>,
102 auth: AuthenticatedUser,
103 organization_id: web::Path<Uuid>,
104) -> impl Responder {
105 if let Err(e) = auth.verify_org_access(*organization_id) {
106 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
107 }
108 match data
109 .achievement_use_cases
110 .list_achievements(organization_id.into_inner())
111 .await
112 {
113 Ok(achievements) => HttpResponse::Ok().json(achievements),
114 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
115 }
116}
117
118#[get("/organizations/{organization_id}/achievements/category/{category}")]
125pub async fn list_achievements_by_category(
126 data: web::Data<AppState>,
127 auth: AuthenticatedUser,
128 path: web::Path<(Uuid, String)>,
129) -> impl Responder {
130 let (organization_id, category_str) = path.into_inner();
131 if let Err(e) = auth.verify_org_access(organization_id) {
132 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
133 }
134
135 let category: AchievementCategory = match serde_json::from_str(&format!("\"{}\"", category_str))
137 {
138 Ok(cat) => cat,
139 Err(_) => {
140 return HttpResponse::BadRequest()
141 .json(serde_json::json!({"error": "Invalid category"}))
142 }
143 };
144
145 match data
146 .achievement_use_cases
147 .list_achievements_by_category(organization_id, category)
148 .await
149 {
150 Ok(achievements) => HttpResponse::Ok().json(achievements),
151 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
152 }
153}
154
155#[get("/organizations/{organization_id}/achievements/visible")]
162pub async fn list_visible_achievements(
163 data: web::Data<AppState>,
164 auth: AuthenticatedUser,
165 organization_id: web::Path<Uuid>,
166) -> impl Responder {
167 if let Err(e) = auth.verify_org_access(*organization_id) {
168 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
169 }
170 match data
171 .achievement_use_cases
172 .list_visible_achievements(organization_id.into_inner(), auth.user_id)
173 .await
174 {
175 Ok(achievements) => HttpResponse::Ok().json(achievements),
176 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
177 }
178}
179
180#[put("/achievements/{id}")]
189pub async fn update_achievement(
190 data: web::Data<AppState>,
191 auth: AuthenticatedUser,
192 id: web::Path<Uuid>,
193 request: web::Json<UpdateAchievementDto>,
194) -> impl Responder {
195 if !auth.is_superadmin() && auth.role != "syndic" {
196 return HttpResponse::Forbidden().json(serde_json::json!({
197 "error": "Only superadmin or syndic can update achievements"
198 }));
199 }
200 match data
201 .achievement_use_cases
202 .update_achievement(id.into_inner(), request.into_inner())
203 .await
204 {
205 Ok(achievement) => HttpResponse::Ok().json(achievement),
206 Err(e) if classification_erreurs::est_introuvable(&e) => {
207 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
208 }
209 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
210 }
211}
212
213#[delete("/achievements/{id}")]
221pub async fn delete_achievement(
222 data: web::Data<AppState>,
223 auth: AuthenticatedUser,
224 id: web::Path<Uuid>,
225) -> impl Responder {
226 if !auth.is_superadmin() && auth.role != "syndic" {
227 return HttpResponse::Forbidden().json(serde_json::json!({
228 "error": "Only superadmin or syndic can delete achievements"
229 }));
230 }
231 match data
232 .achievement_use_cases
233 .delete_achievement(id.into_inner())
234 .await
235 {
236 Ok(_) => HttpResponse::NoContent().finish(),
237 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
238 }
239}
240
241#[derive(Debug, Deserialize)]
246pub struct AwardAchievementRequest {
247 pub achievement_id: Uuid,
248 pub progress_data: Option<String>,
249}
250
251#[post("/users/achievements")]
264pub async fn award_achievement(
265 data: web::Data<AppState>,
266 auth: AuthenticatedUser,
267 request: web::Json<AwardAchievementRequest>,
268) -> impl Responder {
269 let req = request.into_inner();
270 match data
271 .achievement_use_cases
272 .award_achievement(auth.user_id, req.achievement_id, req.progress_data)
273 .await
274 {
275 Ok(user_achievement) => HttpResponse::Created().json(user_achievement),
276 Err(e) if classification_erreurs::est_introuvable(&e) => {
277 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
278 }
279 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
280 }
281}
282
283#[get("/users/achievements")]
290pub async fn get_user_achievements(
291 data: web::Data<AppState>,
292 auth: AuthenticatedUser,
293) -> impl Responder {
294 match data
295 .achievement_use_cases
296 .get_user_achievements(auth.user_id)
297 .await
298 {
299 Ok(achievements) => HttpResponse::Ok().json(achievements),
300 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
301 }
302}
303
304#[get("/users/achievements/recent")]
314pub async fn get_recent_achievements(
315 data: web::Data<AppState>,
316 auth: AuthenticatedUser,
317 query: web::Query<serde_json::Value>,
318) -> impl Responder {
319 let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
320
321 match data
322 .achievement_use_cases
323 .get_recent_achievements(auth.user_id, limit)
324 .await
325 {
326 Ok(achievements) => HttpResponse::Ok().json(achievements),
327 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
328 }
329}
330
331#[post("/challenges")]
356pub async fn create_challenge(
357 data: web::Data<AppState>,
358 auth: AuthenticatedUser,
359 request: web::Json<CreateChallengeDto>,
360) -> impl Responder {
361 if !auth.is_superadmin() && auth.role != "syndic" {
362 return HttpResponse::Forbidden().json(serde_json::json!({
363 "error": "Only superadmin or syndic can create challenges"
364 }));
365 }
366 match data
367 .challenge_use_cases
368 .create_challenge(request.into_inner())
369 .await
370 {
371 Ok(challenge) => HttpResponse::Created().json(challenge),
372 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
373 }
374}
375
376#[get("/challenges/{id}")]
384pub async fn get_challenge(
385 data: web::Data<AppState>,
386 auth: AuthenticatedUser,
387 id: web::Path<Uuid>,
388) -> impl Responder {
389 if let Err(err) = verify_challenge_org_access(&auth, *id, &data.challenge_use_cases).await {
393 return err.error_response();
394 }
395
396 match data
397 .challenge_use_cases
398 .get_challenge(id.into_inner())
399 .await
400 {
401 Ok(challenge) => HttpResponse::Ok().json(challenge),
402 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
403 }
404}
405
406#[get("/organizations/{organization_id}/challenges")]
413pub async fn list_challenges(
414 data: web::Data<AppState>,
415 auth: AuthenticatedUser,
416 organization_id: web::Path<Uuid>,
417) -> impl Responder {
418 if let Err(e) = auth.verify_org_access(*organization_id) {
419 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
420 }
421 match data
422 .challenge_use_cases
423 .list_challenges(organization_id.into_inner())
424 .await
425 {
426 Ok(challenges) => HttpResponse::Ok().json(challenges),
427 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
428 }
429}
430
431#[get("/organizations/{organization_id}/challenges/status/{status}")]
438pub async fn list_challenges_by_status(
439 data: web::Data<AppState>,
440 auth: AuthenticatedUser,
441 path: web::Path<(Uuid, String)>,
442) -> impl Responder {
443 let (organization_id, status_str) = path.into_inner();
444 if let Err(e) = auth.verify_org_access(organization_id) {
445 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
446 }
447
448 let status: ChallengeStatus = match serde_json::from_str(&format!("\"{}\"", status_str)) {
450 Ok(s) => s,
451 Err(_) => {
452 return HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid status"}))
453 }
454 };
455
456 match data
457 .challenge_use_cases
458 .list_challenges_by_status(organization_id, status)
459 .await
460 {
461 Ok(challenges) => HttpResponse::Ok().json(challenges),
462 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
463 }
464}
465
466#[get("/buildings/{building_id}/challenges")]
473pub async fn list_building_challenges(
474 data: web::Data<AppState>,
475 auth: AuthenticatedUser,
476 building_id: web::Path<Uuid>,
477) -> impl Responder {
478 if let Err(err) = verify_building_org_access(
481 &auth,
482 *building_id,
483 &data.building_use_cases,
484 &data.acp_use_cases,
485 )
486 .await
487 {
488 return err.error_response();
489 }
490
491 match data
492 .challenge_use_cases
493 .list_building_challenges(building_id.into_inner())
494 .await
495 {
496 Ok(challenges) => HttpResponse::Ok().json(challenges),
497 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
498 }
499}
500
501#[get("/organizations/{organization_id}/challenges/active")]
508pub async fn list_active_challenges(
509 data: web::Data<AppState>,
510 auth: AuthenticatedUser,
511 organization_id: web::Path<Uuid>,
512) -> impl Responder {
513 if let Err(e) = auth.verify_org_access(*organization_id) {
514 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
515 }
516 match data
517 .challenge_use_cases
518 .list_active_challenges(organization_id.into_inner())
519 .await
520 {
521 Ok(challenges) => HttpResponse::Ok().json(challenges),
522 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
523 }
524}
525
526#[put("/challenges/{id}")]
535pub async fn update_challenge(
536 data: web::Data<AppState>,
537 auth: AuthenticatedUser,
538 id: web::Path<Uuid>,
539 request: web::Json<UpdateChallengeDto>,
540) -> impl Responder {
541 if !auth.is_superadmin() && auth.role != "syndic" {
542 return HttpResponse::Forbidden().json(serde_json::json!({
543 "error": "Only superadmin or syndic can update challenges"
544 }));
545 }
546 match data
547 .challenge_use_cases
548 .update_challenge(id.into_inner(), request.into_inner())
549 .await
550 {
551 Ok(challenge) => HttpResponse::Ok().json(challenge),
552 Err(e) if classification_erreurs::est_introuvable(&e) => {
553 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
554 }
555 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
556 }
557}
558
559#[put("/challenges/{id}/activate")]
568pub async fn activate_challenge(
569 data: web::Data<AppState>,
570 auth: AuthenticatedUser,
571 id: web::Path<Uuid>,
572) -> impl Responder {
573 if !auth.is_superadmin() && auth.role != "syndic" {
574 return HttpResponse::Forbidden().json(serde_json::json!({
575 "error": "Only superadmin or syndic can activate challenges"
576 }));
577 }
578 match data
579 .challenge_use_cases
580 .activate_challenge(id.into_inner())
581 .await
582 {
583 Ok(challenge) => HttpResponse::Ok().json(challenge),
584 Err(e) if classification_erreurs::est_introuvable(&e) => {
585 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
586 }
587 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
588 }
589}
590
591#[put("/challenges/{id}/complete")]
600pub async fn complete_challenge(
601 data: web::Data<AppState>,
602 auth: AuthenticatedUser,
603 id: web::Path<Uuid>,
604) -> impl Responder {
605 if !auth.is_superadmin() && auth.role != "syndic" {
606 return HttpResponse::Forbidden().json(serde_json::json!({
607 "error": "Only superadmin or syndic can complete challenges"
608 }));
609 }
610 match data
611 .challenge_use_cases
612 .complete_challenge(id.into_inner())
613 .await
614 {
615 Ok(challenge) => HttpResponse::Ok().json(challenge),
616 Err(e) if classification_erreurs::est_introuvable(&e) => {
617 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
618 }
619 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
620 }
621}
622
623#[put("/challenges/{id}/cancel")]
632pub async fn cancel_challenge(
633 data: web::Data<AppState>,
634 auth: AuthenticatedUser,
635 id: web::Path<Uuid>,
636) -> impl Responder {
637 if !auth.is_superadmin() && auth.role != "syndic" {
638 return HttpResponse::Forbidden().json(serde_json::json!({
639 "error": "Only superadmin or syndic can cancel challenges"
640 }));
641 }
642 match data
643 .challenge_use_cases
644 .cancel_challenge(id.into_inner())
645 .await
646 {
647 Ok(challenge) => HttpResponse::Ok().json(challenge),
648 Err(e) if classification_erreurs::est_introuvable(&e) => {
649 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
650 }
651 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
652 }
653}
654
655#[delete("/challenges/{id}")]
663pub async fn delete_challenge(
664 data: web::Data<AppState>,
665 auth: AuthenticatedUser,
666 id: web::Path<Uuid>,
667) -> impl Responder {
668 if !auth.is_superadmin() && auth.role != "syndic" {
669 return HttpResponse::Forbidden().json(serde_json::json!({
670 "error": "Only superadmin or syndic can delete challenges"
671 }));
672 }
673 match data
674 .challenge_use_cases
675 .delete_challenge(id.into_inner())
676 .await
677 {
678 Ok(_) => HttpResponse::NoContent().finish(),
679 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
680 }
681}
682
683#[get("/challenges/{challenge_id}/progress")]
695pub async fn get_challenge_progress(
696 data: web::Data<AppState>,
697 auth: AuthenticatedUser,
698 challenge_id: web::Path<Uuid>,
699) -> impl Responder {
700 if let Err(err) =
705 verify_challenge_org_access(&auth, *challenge_id, &data.challenge_use_cases).await
706 {
707 return err.error_response();
708 }
709
710 match data
711 .challenge_use_cases
712 .get_challenge_progress(auth.user_id, challenge_id.into_inner())
713 .await
714 {
715 Ok(progress) => HttpResponse::Ok().json(progress),
716 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
717 }
718}
719
720#[get("/challenges/{challenge_id}/all-progress")]
727pub async fn list_challenge_progress(
728 data: web::Data<AppState>,
729 auth: AuthenticatedUser,
730 challenge_id: web::Path<Uuid>,
731) -> impl Responder {
732 if let Err(err) =
737 verify_challenge_org_access(&auth, *challenge_id, &data.challenge_use_cases).await
738 {
739 return err.error_response();
740 }
741
742 match data
743 .challenge_use_cases
744 .list_challenge_progress(challenge_id.into_inner())
745 .await
746 {
747 Ok(progress_list) => HttpResponse::Ok().json(progress_list),
748 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
749 }
750}
751
752#[get("/users/challenges/active")]
759pub async fn list_user_active_challenges(
760 data: web::Data<AppState>,
761 auth: AuthenticatedUser,
762) -> impl Responder {
763 match data
764 .challenge_use_cases
765 .list_user_active_progress(auth.user_id)
766 .await
767 {
768 Ok(progress_list) => HttpResponse::Ok().json(progress_list),
769 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
770 }
771}
772
773#[derive(Debug, Deserialize)]
774pub struct IncrementProgressRequest {
775 pub increment: i32,
776}
777
778#[post("/challenges/{challenge_id}/progress/increment")]
790pub async fn increment_progress(
791 data: web::Data<AppState>,
792 auth: AuthenticatedUser,
793 challenge_id: web::Path<Uuid>,
794 request: web::Json<IncrementProgressRequest>,
795) -> impl Responder {
796 if let Err(err) =
801 verify_challenge_org_access(&auth, *challenge_id, &data.challenge_use_cases).await
802 {
803 return err.error_response();
804 }
805
806 match data
807 .challenge_use_cases
808 .increment_progress(auth.user_id, challenge_id.into_inner(), request.increment)
809 .await
810 {
811 Ok(progress) => HttpResponse::Ok().json(progress),
812 Err(e) if classification_erreurs::est_introuvable(&e) => {
813 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
814 }
815 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
816 }
817}
818
819#[get("/organizations/{organization_id}/gamification/stats")]
830pub async fn get_gamification_user_stats(
831 data: web::Data<AppState>,
832 auth: AuthenticatedUser,
833 organization_id: web::Path<Uuid>,
834) -> impl Responder {
835 if let Err(e) = auth.verify_org_access(*organization_id) {
836 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
837 }
838 match data
839 .gamification_stats_use_cases
840 .get_user_stats(auth.user_id, organization_id.into_inner())
841 .await
842 {
843 Ok(stats) => HttpResponse::Ok().json(stats),
844 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
845 }
846}
847
848#[get("/organizations/{organization_id}/gamification/leaderboard")]
859pub async fn get_gamification_leaderboard(
860 data: web::Data<AppState>,
861 auth: AuthenticatedUser,
862 organization_id: web::Path<Uuid>,
863 query: web::Query<serde_json::Value>,
864) -> impl Responder {
865 if let Err(e) = auth.verify_org_access(*organization_id) {
866 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
867 }
868 let building_id = query
869 .get("building_id")
870 .and_then(|v| v.as_str())
871 .and_then(|s| Uuid::parse_str(s).ok());
872
873 let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(10);
874
875 match data
876 .gamification_stats_use_cases
877 .get_leaderboard(organization_id.into_inner(), building_id, limit)
878 .await
879 {
880 Ok(leaderboard) => HttpResponse::Ok().json(leaderboard),
881 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
882 }
883}