koprogo_api/infrastructure/web/handlers/
gamification_handlers.rs

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::middleware::AuthenticatedUser;
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use serde::Deserialize;
9use uuid::Uuid;
10
11// ============================================================================
12// ACHIEVEMENT HANDLERS
13// ============================================================================
14
15/// Create a new achievement (admin only)
16///
17/// POST /achievements
18///
19/// # Request Body
20/// - organization_id: UUID
21/// - category: AchievementCategory
22/// - tier: AchievementTier
23/// - name: String (3-100 chars)
24/// - description: String (10-500 chars)
25/// - icon: String (emoji or URL)
26/// - points_value: i32 (0-1000)
27/// - requirements: String (JSON criteria)
28/// - is_secret: bool
29/// - is_repeatable: bool
30/// - display_order: i32
31///
32/// # Responses
33/// - 201 Created: Achievement created successfully
34/// - 400 Bad Request: Validation error
35#[post("/achievements")]
36pub async fn create_achievement(
37    data: web::Data<AppState>,
38    auth: AuthenticatedUser,
39    request: web::Json<CreateAchievementDto>,
40) -> impl Responder {
41    if auth.role != "superadmin" && auth.role != "syndic" {
42        return HttpResponse::Forbidden().json(serde_json::json!({
43            "error": "Only superadmin or syndic can create achievements"
44        }));
45    }
46    match data
47        .achievement_use_cases
48        .create_achievement(request.into_inner())
49        .await
50    {
51        Ok(achievement) => HttpResponse::Created().json(achievement),
52        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
53    }
54}
55
56/// Get achievement by ID
57///
58/// GET /achievements/:id
59///
60/// # Responses
61/// - 200 OK: Achievement details
62/// - 404 Not Found: Achievement not found
63#[get("/achievements/{id}")]
64pub async fn get_achievement(
65    data: web::Data<AppState>,
66    _auth: AuthenticatedUser,
67    id: web::Path<Uuid>,
68) -> impl Responder {
69    match data
70        .achievement_use_cases
71        .get_achievement(id.into_inner())
72        .await
73    {
74        Ok(achievement) => HttpResponse::Ok().json(achievement),
75        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
76    }
77}
78
79/// List all achievements for an organization
80///
81/// GET /organizations/:organization_id/achievements
82///
83/// # Responses
84/// - 200 OK: List of achievements
85#[get("/organizations/{organization_id}/achievements")]
86pub async fn list_achievements(
87    data: web::Data<AppState>,
88    auth: AuthenticatedUser,
89    organization_id: web::Path<Uuid>,
90) -> impl Responder {
91    if let Err(e) = auth.verify_org_access(*organization_id) {
92        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
93    }
94    match data
95        .achievement_use_cases
96        .list_achievements(organization_id.into_inner())
97        .await
98    {
99        Ok(achievements) => HttpResponse::Ok().json(achievements),
100        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
101    }
102}
103
104/// List achievements by category
105///
106/// GET /organizations/:organization_id/achievements/category/:category
107///
108/// # Responses
109/// - 200 OK: List of achievements in category
110#[get("/organizations/{organization_id}/achievements/category/{category}")]
111pub async fn list_achievements_by_category(
112    data: web::Data<AppState>,
113    auth: AuthenticatedUser,
114    path: web::Path<(Uuid, String)>,
115) -> impl Responder {
116    let (organization_id, category_str) = path.into_inner();
117    if let Err(e) = auth.verify_org_access(organization_id) {
118        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
119    }
120
121    // Parse category
122    let category: AchievementCategory = match serde_json::from_str(&format!("\"{}\"", category_str))
123    {
124        Ok(cat) => cat,
125        Err(_) => {
126            return HttpResponse::BadRequest()
127                .json(serde_json::json!({"error": "Invalid category"}))
128        }
129    };
130
131    match data
132        .achievement_use_cases
133        .list_achievements_by_category(organization_id, category)
134        .await
135    {
136        Ok(achievements) => HttpResponse::Ok().json(achievements),
137        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
138    }
139}
140
141/// List visible achievements for current user
142///
143/// GET /organizations/:organization_id/achievements/visible
144///
145/// # Responses
146/// - 200 OK: List of visible achievements (non-secret or already earned)
147#[get("/organizations/{organization_id}/achievements/visible")]
148pub async fn list_visible_achievements(
149    data: web::Data<AppState>,
150    auth: AuthenticatedUser,
151    organization_id: web::Path<Uuid>,
152) -> impl Responder {
153    if let Err(e) = auth.verify_org_access(*organization_id) {
154        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
155    }
156    match data
157        .achievement_use_cases
158        .list_visible_achievements(organization_id.into_inner(), auth.user_id)
159        .await
160    {
161        Ok(achievements) => HttpResponse::Ok().json(achievements),
162        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
163    }
164}
165
166/// Update achievement (admin only)
167///
168/// PUT /achievements/:id
169///
170/// # Responses
171/// - 200 OK: Achievement updated successfully
172/// - 400 Bad Request: Validation error
173/// - 404 Not Found: Achievement not found
174#[put("/achievements/{id}")]
175pub async fn update_achievement(
176    data: web::Data<AppState>,
177    auth: AuthenticatedUser,
178    id: web::Path<Uuid>,
179    request: web::Json<UpdateAchievementDto>,
180) -> impl Responder {
181    if auth.role != "superadmin" && auth.role != "syndic" {
182        return HttpResponse::Forbidden().json(serde_json::json!({
183            "error": "Only superadmin or syndic can update achievements"
184        }));
185    }
186    match data
187        .achievement_use_cases
188        .update_achievement(id.into_inner(), request.into_inner())
189        .await
190    {
191        Ok(achievement) => HttpResponse::Ok().json(achievement),
192        Err(e) if e.contains("not found") => {
193            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
194        }
195        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
196    }
197}
198
199/// Delete achievement (admin only)
200///
201/// DELETE /achievements/:id
202///
203/// # Responses
204/// - 204 No Content: Achievement deleted successfully
205/// - 404 Not Found: Achievement not found
206#[delete("/achievements/{id}")]
207pub async fn delete_achievement(
208    data: web::Data<AppState>,
209    auth: AuthenticatedUser,
210    id: web::Path<Uuid>,
211) -> impl Responder {
212    if auth.role != "superadmin" && auth.role != "syndic" {
213        return HttpResponse::Forbidden().json(serde_json::json!({
214            "error": "Only superadmin or syndic can delete achievements"
215        }));
216    }
217    match data
218        .achievement_use_cases
219        .delete_achievement(id.into_inner())
220        .await
221    {
222        Ok(_) => HttpResponse::NoContent().finish(),
223        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
224    }
225}
226
227// ============================================================================
228// USER ACHIEVEMENT HANDLERS
229// ============================================================================
230
231#[derive(Debug, Deserialize)]
232pub struct AwardAchievementRequest {
233    pub achievement_id: Uuid,
234    pub progress_data: Option<String>,
235}
236
237/// Award achievement to user
238///
239/// POST /users/achievements
240///
241/// # Request Body
242/// - achievement_id: UUID
243/// - progress_data: `Option<String>` (JSON)
244///
245/// # Responses
246/// - 201 Created: Achievement awarded successfully
247/// - 400 Bad Request: Already earned (non-repeatable) or validation error
248/// - 404 Not Found: Achievement not found
249#[post("/users/achievements")]
250pub async fn award_achievement(
251    data: web::Data<AppState>,
252    auth: AuthenticatedUser,
253    request: web::Json<AwardAchievementRequest>,
254) -> impl Responder {
255    let req = request.into_inner();
256    match data
257        .achievement_use_cases
258        .award_achievement(auth.user_id, req.achievement_id, req.progress_data)
259        .await
260    {
261        Ok(user_achievement) => HttpResponse::Created().json(user_achievement),
262        Err(e) if e.contains("not found") => {
263            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
264        }
265        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
266    }
267}
268
269/// Get all achievements earned by current user
270///
271/// GET /users/achievements
272///
273/// # Responses
274/// - 200 OK: List of user achievements with enriched achievement data
275#[get("/users/achievements")]
276pub async fn get_user_achievements(
277    data: web::Data<AppState>,
278    auth: AuthenticatedUser,
279) -> impl Responder {
280    match data
281        .achievement_use_cases
282        .get_user_achievements(auth.user_id)
283        .await
284    {
285        Ok(achievements) => HttpResponse::Ok().json(achievements),
286        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
287    }
288}
289
290/// Get recent achievements for current user
291///
292/// GET /users/achievements/recent?limit=5
293///
294/// # Query Parameters
295/// - limit: i64 (default: 5)
296///
297/// # Responses
298/// - 200 OK: List of recent achievements
299#[get("/users/achievements/recent")]
300pub async fn get_recent_achievements(
301    data: web::Data<AppState>,
302    auth: AuthenticatedUser,
303    query: web::Query<serde_json::Value>,
304) -> impl Responder {
305    let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(5);
306
307    match data
308        .achievement_use_cases
309        .get_recent_achievements(auth.user_id, limit)
310        .await
311    {
312        Ok(achievements) => HttpResponse::Ok().json(achievements),
313        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
314    }
315}
316
317// ============================================================================
318// CHALLENGE HANDLERS
319// ============================================================================
320
321/// Create a new challenge (admin only)
322///
323/// POST /challenges
324///
325/// # Request Body
326/// - organization_id: UUID
327/// - building_id: `Option<UUID>` (null = organization-wide)
328/// - challenge_type: ChallengeType (Individual, Team, Building)
329/// - title: String (3-100 chars)
330/// - description: String (10-1000 chars)
331/// - icon: String
332/// - start_date: DateTime<Utc>
333/// - end_date: DateTime<Utc>
334/// - target_metric: String (e.g., "bookings_created")
335/// - target_value: i32
336/// - reward_points: i32 (0-10000)
337///
338/// # Responses
339/// - 201 Created: Challenge created successfully (Draft status)
340/// - 400 Bad Request: Validation error
341#[post("/challenges")]
342pub async fn create_challenge(
343    data: web::Data<AppState>,
344    auth: AuthenticatedUser,
345    request: web::Json<CreateChallengeDto>,
346) -> impl Responder {
347    if auth.role != "superadmin" && auth.role != "syndic" {
348        return HttpResponse::Forbidden().json(serde_json::json!({
349            "error": "Only superadmin or syndic can create challenges"
350        }));
351    }
352    match data
353        .challenge_use_cases
354        .create_challenge(request.into_inner())
355        .await
356    {
357        Ok(challenge) => HttpResponse::Created().json(challenge),
358        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
359    }
360}
361
362/// Get challenge by ID
363///
364/// GET /challenges/:id
365///
366/// # Responses
367/// - 200 OK: Challenge details
368/// - 404 Not Found: Challenge not found
369#[get("/challenges/{id}")]
370pub async fn get_challenge(
371    data: web::Data<AppState>,
372    _auth: AuthenticatedUser,
373    id: web::Path<Uuid>,
374) -> impl Responder {
375    match data
376        .challenge_use_cases
377        .get_challenge(id.into_inner())
378        .await
379    {
380        Ok(challenge) => HttpResponse::Ok().json(challenge),
381        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
382    }
383}
384
385/// List all challenges for an organization
386///
387/// GET /organizations/:organization_id/challenges
388///
389/// # Responses
390/// - 200 OK: List of challenges
391#[get("/organizations/{organization_id}/challenges")]
392pub async fn list_challenges(
393    data: web::Data<AppState>,
394    auth: AuthenticatedUser,
395    organization_id: web::Path<Uuid>,
396) -> impl Responder {
397    if let Err(e) = auth.verify_org_access(*organization_id) {
398        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
399    }
400    match data
401        .challenge_use_cases
402        .list_challenges(organization_id.into_inner())
403        .await
404    {
405        Ok(challenges) => HttpResponse::Ok().json(challenges),
406        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
407    }
408}
409
410/// List challenges by status
411///
412/// GET /organizations/:organization_id/challenges/status/:status
413///
414/// # Responses
415/// - 200 OK: List of challenges with specified status
416#[get("/organizations/{organization_id}/challenges/status/{status}")]
417pub async fn list_challenges_by_status(
418    data: web::Data<AppState>,
419    auth: AuthenticatedUser,
420    path: web::Path<(Uuid, String)>,
421) -> impl Responder {
422    let (organization_id, status_str) = path.into_inner();
423    if let Err(e) = auth.verify_org_access(organization_id) {
424        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
425    }
426
427    // Parse status
428    let status: ChallengeStatus = match serde_json::from_str(&format!("\"{}\"", status_str)) {
429        Ok(s) => s,
430        Err(_) => {
431            return HttpResponse::BadRequest().json(serde_json::json!({"error": "Invalid status"}))
432        }
433    };
434
435    match data
436        .challenge_use_cases
437        .list_challenges_by_status(organization_id, status)
438        .await
439    {
440        Ok(challenges) => HttpResponse::Ok().json(challenges),
441        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
442    }
443}
444
445/// List challenges for a building
446///
447/// GET /buildings/:building_id/challenges
448///
449/// # Responses
450/// - 200 OK: List of building challenges
451#[get("/buildings/{building_id}/challenges")]
452pub async fn list_building_challenges(
453    data: web::Data<AppState>,
454    _auth: AuthenticatedUser,
455    building_id: web::Path<Uuid>,
456) -> impl Responder {
457    match data
458        .challenge_use_cases
459        .list_building_challenges(building_id.into_inner())
460        .await
461    {
462        Ok(challenges) => HttpResponse::Ok().json(challenges),
463        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
464    }
465}
466
467/// List active challenges (Active status + date range)
468///
469/// GET /organizations/:organization_id/challenges/active
470///
471/// # Responses
472/// - 200 OK: List of currently active challenges
473#[get("/organizations/{organization_id}/challenges/active")]
474pub async fn list_active_challenges(
475    data: web::Data<AppState>,
476    auth: AuthenticatedUser,
477    organization_id: web::Path<Uuid>,
478) -> impl Responder {
479    if let Err(e) = auth.verify_org_access(*organization_id) {
480        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
481    }
482    match data
483        .challenge_use_cases
484        .list_active_challenges(organization_id.into_inner())
485        .await
486    {
487        Ok(challenges) => HttpResponse::Ok().json(challenges),
488        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
489    }
490}
491
492/// Update challenge (Draft only, admin only)
493///
494/// PUT /challenges/:id
495///
496/// # Responses
497/// - 200 OK: Challenge updated successfully
498/// - 400 Bad Request: Validation error or not Draft status
499/// - 404 Not Found: Challenge not found
500#[put("/challenges/{id}")]
501pub async fn update_challenge(
502    data: web::Data<AppState>,
503    auth: AuthenticatedUser,
504    id: web::Path<Uuid>,
505    request: web::Json<UpdateChallengeDto>,
506) -> impl Responder {
507    if auth.role != "superadmin" && auth.role != "syndic" {
508        return HttpResponse::Forbidden().json(serde_json::json!({
509            "error": "Only superadmin or syndic can update challenges"
510        }));
511    }
512    match data
513        .challenge_use_cases
514        .update_challenge(id.into_inner(), request.into_inner())
515        .await
516    {
517        Ok(challenge) => HttpResponse::Ok().json(challenge),
518        Err(e) if e.contains("not found") => {
519            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
520        }
521        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
522    }
523}
524
525/// Activate challenge (Draft → Active, admin only)
526///
527/// PUT /challenges/:id/activate
528///
529/// # Responses
530/// - 200 OK: Challenge activated successfully
531/// - 400 Bad Request: Invalid state transition
532/// - 404 Not Found: Challenge not found
533#[put("/challenges/{id}/activate")]
534pub async fn activate_challenge(
535    data: web::Data<AppState>,
536    auth: AuthenticatedUser,
537    id: web::Path<Uuid>,
538) -> impl Responder {
539    if auth.role != "superadmin" && auth.role != "syndic" {
540        return HttpResponse::Forbidden().json(serde_json::json!({
541            "error": "Only superadmin or syndic can activate challenges"
542        }));
543    }
544    match data
545        .challenge_use_cases
546        .activate_challenge(id.into_inner())
547        .await
548    {
549        Ok(challenge) => HttpResponse::Ok().json(challenge),
550        Err(e) if e.contains("not found") => {
551            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
552        }
553        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
554    }
555}
556
557/// Complete challenge (Active → Completed, admin only)
558///
559/// PUT /challenges/:id/complete
560///
561/// # Responses
562/// - 200 OK: Challenge completed successfully
563/// - 400 Bad Request: Invalid state transition
564/// - 404 Not Found: Challenge not found
565#[put("/challenges/{id}/complete")]
566pub async fn complete_challenge(
567    data: web::Data<AppState>,
568    auth: AuthenticatedUser,
569    id: web::Path<Uuid>,
570) -> impl Responder {
571    if auth.role != "superadmin" && auth.role != "syndic" {
572        return HttpResponse::Forbidden().json(serde_json::json!({
573            "error": "Only superadmin or syndic can complete challenges"
574        }));
575    }
576    match data
577        .challenge_use_cases
578        .complete_challenge(id.into_inner())
579        .await
580    {
581        Ok(challenge) => HttpResponse::Ok().json(challenge),
582        Err(e) if e.contains("not found") => {
583            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
584        }
585        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
586    }
587}
588
589/// Cancel challenge (Draft/Active → Cancelled, admin only)
590///
591/// PUT /challenges/:id/cancel
592///
593/// # Responses
594/// - 200 OK: Challenge cancelled successfully
595/// - 400 Bad Request: Invalid state transition
596/// - 404 Not Found: Challenge not found
597#[put("/challenges/{id}/cancel")]
598pub async fn cancel_challenge(
599    data: web::Data<AppState>,
600    auth: AuthenticatedUser,
601    id: web::Path<Uuid>,
602) -> impl Responder {
603    if auth.role != "superadmin" && auth.role != "syndic" {
604        return HttpResponse::Forbidden().json(serde_json::json!({
605            "error": "Only superadmin or syndic can cancel challenges"
606        }));
607    }
608    match data
609        .challenge_use_cases
610        .cancel_challenge(id.into_inner())
611        .await
612    {
613        Ok(challenge) => HttpResponse::Ok().json(challenge),
614        Err(e) if e.contains("not found") => {
615            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
616        }
617        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
618    }
619}
620
621/// Delete challenge (admin only)
622///
623/// DELETE /challenges/:id
624///
625/// # Responses
626/// - 204 No Content: Challenge deleted successfully
627/// - 404 Not Found: Challenge not found
628#[delete("/challenges/{id}")]
629pub async fn delete_challenge(
630    data: web::Data<AppState>,
631    auth: AuthenticatedUser,
632    id: web::Path<Uuid>,
633) -> impl Responder {
634    if auth.role != "superadmin" && auth.role != "syndic" {
635        return HttpResponse::Forbidden().json(serde_json::json!({
636            "error": "Only superadmin or syndic can delete challenges"
637        }));
638    }
639    match data
640        .challenge_use_cases
641        .delete_challenge(id.into_inner())
642        .await
643    {
644        Ok(_) => HttpResponse::NoContent().finish(),
645        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
646    }
647}
648
649// ============================================================================
650// CHALLENGE PROGRESS HANDLERS
651// ============================================================================
652
653/// Get user progress for a challenge
654///
655/// GET /challenges/:challenge_id/progress
656///
657/// # Responses
658/// - 200 OK: User progress details
659/// - 404 Not Found: Progress or challenge not found
660#[get("/challenges/{challenge_id}/progress")]
661pub async fn get_challenge_progress(
662    data: web::Data<AppState>,
663    auth: AuthenticatedUser,
664    challenge_id: web::Path<Uuid>,
665) -> impl Responder {
666    match data
667        .challenge_use_cases
668        .get_challenge_progress(auth.user_id, challenge_id.into_inner())
669        .await
670    {
671        Ok(progress) => HttpResponse::Ok().json(progress),
672        Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
673    }
674}
675
676/// List all progress for a challenge
677///
678/// GET /challenges/:challenge_id/all-progress
679///
680/// # Responses
681/// - 200 OK: List of all user progress for challenge
682#[get("/challenges/{challenge_id}/all-progress")]
683pub async fn list_challenge_progress(
684    data: web::Data<AppState>,
685    _auth: AuthenticatedUser,
686    challenge_id: web::Path<Uuid>,
687) -> impl Responder {
688    match data
689        .challenge_use_cases
690        .list_challenge_progress(challenge_id.into_inner())
691        .await
692    {
693        Ok(progress_list) => HttpResponse::Ok().json(progress_list),
694        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
695    }
696}
697
698/// List active challenges for current user with progress
699///
700/// GET /users/challenges/active
701///
702/// # Responses
703/// - 200 OK: List of active challenges with user progress
704#[get("/users/challenges/active")]
705pub async fn list_user_active_challenges(
706    data: web::Data<AppState>,
707    auth: AuthenticatedUser,
708) -> impl Responder {
709    match data
710        .challenge_use_cases
711        .list_user_active_progress(auth.user_id)
712        .await
713    {
714        Ok(progress_list) => HttpResponse::Ok().json(progress_list),
715        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
716    }
717}
718
719#[derive(Debug, Deserialize)]
720pub struct IncrementProgressRequest {
721    pub increment: i32,
722}
723
724/// Increment user progress for a challenge
725///
726/// POST /challenges/:challenge_id/progress/increment
727///
728/// # Request Body
729/// - increment: i32 (value to add to current progress)
730///
731/// # Responses
732/// - 200 OK: Progress incremented successfully (auto-completes if target reached)
733/// - 400 Bad Request: Validation error
734/// - 404 Not Found: Challenge not found
735#[post("/challenges/{challenge_id}/progress/increment")]
736pub async fn increment_progress(
737    data: web::Data<AppState>,
738    auth: AuthenticatedUser,
739    challenge_id: web::Path<Uuid>,
740    request: web::Json<IncrementProgressRequest>,
741) -> impl Responder {
742    match data
743        .challenge_use_cases
744        .increment_progress(auth.user_id, challenge_id.into_inner(), request.increment)
745        .await
746    {
747        Ok(progress) => HttpResponse::Ok().json(progress),
748        Err(e) if e.contains("not found") => {
749            HttpResponse::NotFound().json(serde_json::json!({"error": e}))
750        }
751        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
752    }
753}
754
755// ============================================================================
756// GAMIFICATION STATS HANDLERS
757// ============================================================================
758
759/// Get comprehensive gamification stats for current user
760///
761/// GET /organizations/:organization_id/gamification/stats
762///
763/// # Responses
764/// - 200 OK: User gamification statistics
765#[get("/organizations/{organization_id}/gamification/stats")]
766pub async fn get_gamification_user_stats(
767    data: web::Data<AppState>,
768    auth: AuthenticatedUser,
769    organization_id: web::Path<Uuid>,
770) -> impl Responder {
771    if let Err(e) = auth.verify_org_access(*organization_id) {
772        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
773    }
774    match data
775        .gamification_stats_use_cases
776        .get_user_stats(auth.user_id, organization_id.into_inner())
777        .await
778    {
779        Ok(stats) => HttpResponse::Ok().json(stats),
780        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
781    }
782}
783
784/// Get leaderboard for organization or building
785///
786/// GET /organizations/:organization_id/gamification/leaderboard?building_id=<uuid>&limit=10
787///
788/// # Query Parameters
789/// - building_id: `Option<UUID>` (filter by building)
790/// - limit: `i64` (default: 10)
791///
792/// # Responses
793/// - 200 OK: Leaderboard with top users
794#[get("/organizations/{organization_id}/gamification/leaderboard")]
795pub async fn get_gamification_leaderboard(
796    data: web::Data<AppState>,
797    auth: AuthenticatedUser,
798    organization_id: web::Path<Uuid>,
799    query: web::Query<serde_json::Value>,
800) -> impl Responder {
801    if let Err(e) = auth.verify_org_access(*organization_id) {
802        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
803    }
804    let building_id = query
805        .get("building_id")
806        .and_then(|v| v.as_str())
807        .and_then(|s| Uuid::parse_str(s).ok());
808
809    let limit = query.get("limit").and_then(|v| v.as_i64()).unwrap_or(10);
810
811    match data
812        .gamification_stats_use_cases
813        .get_leaderboard(organization_id.into_inner(), building_id, limit)
814        .await
815    {
816        Ok(leaderboard) => HttpResponse::Ok().json(leaderboard),
817        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
818    }
819}