1use crate::application::ports::{ChallengeProgressRepository, ChallengeRepository};
2use crate::domain::entities::{Challenge, ChallengeProgress, ChallengeStatus, ChallengeType};
3use crate::infrastructure::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresChallengeRepository {
9 pool: DbPool,
10}
11
12impl PostgresChallengeRepository {
13 pub fn new(pool: DbPool) -> Self {
14 Self { pool }
15 }
16
17 fn row_to_challenge(row: &sqlx::postgres::PgRow) -> Result<Challenge, String> {
19 let challenge_type_str: String = row
21 .try_get("challenge_type")
22 .map_err(|e| format!("Failed to get challenge_type: {}", e))?;
23 let challenge_type: ChallengeType =
24 serde_json::from_str(&format!("\"{}\"", challenge_type_str))
25 .map_err(|e| format!("Failed to parse challenge_type: {}", e))?;
26
27 let status_str: String = row
28 .try_get("status")
29 .map_err(|e| format!("Failed to get status: {}", e))?;
30 let status: ChallengeStatus = serde_json::from_str(&format!("\"{}\"", status_str))
31 .map_err(|e| format!("Failed to parse status: {}", e))?;
32
33 Ok(Challenge {
34 id: row
35 .try_get("id")
36 .map_err(|e| format!("Failed to get id: {}", e))?,
37 organization_id: row
38 .try_get("organization_id")
39 .map_err(|e| format!("Failed to get organization_id: {}", e))?,
40 building_id: row
41 .try_get("building_id")
42 .map_err(|e| format!("Failed to get building_id: {}", e))?,
43 challenge_type,
44 status,
45 title: row
46 .try_get("title")
47 .map_err(|e| format!("Failed to get title: {}", e))?,
48 description: row
49 .try_get("description")
50 .map_err(|e| format!("Failed to get description: {}", e))?,
51 icon: row
52 .try_get("icon")
53 .map_err(|e| format!("Failed to get icon: {}", e))?,
54 start_date: row
55 .try_get("start_date")
56 .map_err(|e| format!("Failed to get start_date: {}", e))?,
57 end_date: row
58 .try_get("end_date")
59 .map_err(|e| format!("Failed to get end_date: {}", e))?,
60 target_metric: row
61 .try_get("target_metric")
62 .map_err(|e| format!("Failed to get target_metric: {}", e))?,
63 target_value: row
64 .try_get("target_value")
65 .map_err(|e| format!("Failed to get target_value: {}", e))?,
66 reward_points: row
67 .try_get("reward_points")
68 .map_err(|e| format!("Failed to get reward_points: {}", e))?,
69 created_at: row
70 .try_get("created_at")
71 .map_err(|e| format!("Failed to get created_at: {}", e))?,
72 updated_at: row
73 .try_get("updated_at")
74 .map_err(|e| format!("Failed to get updated_at: {}", e))?,
75 })
76 }
77}
78
79#[async_trait]
80impl ChallengeRepository for PostgresChallengeRepository {
81 async fn create(&self, challenge: &Challenge) -> Result<Challenge, String> {
82 let challenge_type_str = serde_json::to_string(&challenge.challenge_type)
84 .map_err(|e| format!("Failed to serialize challenge_type: {}", e))?
85 .trim_matches('"')
86 .to_string();
87
88 let status_str = serde_json::to_string(&challenge.status)
89 .map_err(|e| format!("Failed to serialize status: {}", e))?
90 .trim_matches('"')
91 .to_string();
92
93 sqlx::query(
94 r#"
95 INSERT INTO challenges (
96 id, organization_id, building_id, challenge_type, status, title,
97 description, icon, start_date, end_date, target_metric,
98 target_value, reward_points, created_at, updated_at
99 )
100 VALUES ($1, $2, $3, $4::challenge_type, $5::challenge_status, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
101 "#,
102 )
103 .bind(&challenge.id)
104 .bind(&challenge.organization_id)
105 .bind(&challenge.building_id)
106 .bind(&challenge_type_str)
107 .bind(&status_str)
108 .bind(&challenge.title)
109 .bind(&challenge.description)
110 .bind(&challenge.icon)
111 .bind(&challenge.start_date)
112 .bind(&challenge.end_date)
113 .bind(&challenge.target_metric)
114 .bind(&challenge.target_value)
115 .bind(&challenge.reward_points)
116 .bind(&challenge.created_at)
117 .bind(&challenge.updated_at)
118 .execute(&self.pool)
119 .await
120 .map_err(|e| format!("Failed to create challenge: {}", e))?;
121
122 Ok(challenge.clone())
123 }
124
125 async fn find_by_id(&self, id: Uuid) -> Result<Option<Challenge>, String> {
126 let row = sqlx::query(
127 r#"
128 SELECT id, organization_id, building_id, challenge_type, status, title,
129 description, icon, start_date, end_date, target_metric,
130 target_value, reward_points, created_at, updated_at
131 FROM challenges
132 WHERE id = $1
133 "#,
134 )
135 .bind(id)
136 .fetch_optional(&self.pool)
137 .await
138 .map_err(|e| format!("Failed to find challenge by id: {}", e))?;
139
140 row.as_ref().map(Self::row_to_challenge).transpose()
141 }
142
143 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Challenge>, String> {
144 let rows = sqlx::query(
145 r#"
146 SELECT id, organization_id, building_id, challenge_type, status, title,
147 description, icon, start_date, end_date, target_metric,
148 target_value, reward_points, created_at, updated_at
149 FROM challenges
150 WHERE organization_id = $1
151 ORDER BY start_date DESC
152 "#,
153 )
154 .bind(organization_id)
155 .fetch_all(&self.pool)
156 .await
157 .map_err(|e| format!("Failed to find challenges by organization: {}", e))?;
158
159 rows.iter().map(Self::row_to_challenge).collect()
160 }
161
162 async fn find_by_organization_and_status(
163 &self,
164 organization_id: Uuid,
165 status: ChallengeStatus,
166 ) -> Result<Vec<Challenge>, String> {
167 let status_str = serde_json::to_string(&status)
168 .map_err(|e| format!("Failed to serialize status: {}", e))?
169 .trim_matches('"')
170 .to_string();
171
172 let rows = sqlx::query(
173 r#"
174 SELECT id, organization_id, building_id, challenge_type, status, title,
175 description, icon, start_date, end_date, target_metric,
176 target_value, reward_points, created_at, updated_at
177 FROM challenges
178 WHERE organization_id = $1
179 AND status = $2::challenge_status
180 ORDER BY start_date DESC
181 "#,
182 )
183 .bind(organization_id)
184 .bind(&status_str)
185 .fetch_all(&self.pool)
186 .await
187 .map_err(|e| format!("Failed to find challenges by status: {}", e))?;
188
189 rows.iter().map(Self::row_to_challenge).collect()
190 }
191
192 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Challenge>, String> {
193 let rows = sqlx::query(
194 r#"
195 SELECT id, organization_id, building_id, challenge_type, status, title,
196 description, icon, start_date, end_date, target_metric,
197 target_value, reward_points, created_at, updated_at
198 FROM challenges
199 WHERE building_id = $1
200 ORDER BY start_date DESC
201 "#,
202 )
203 .bind(building_id)
204 .fetch_all(&self.pool)
205 .await
206 .map_err(|e| format!("Failed to find challenges by building: {}", e))?;
207
208 rows.iter().map(Self::row_to_challenge).collect()
209 }
210
211 async fn find_active(&self, organization_id: Uuid) -> Result<Vec<Challenge>, String> {
212 let rows = sqlx::query(
213 r#"
214 SELECT id, organization_id, building_id, challenge_type, status, title,
215 description, icon, start_date, end_date, target_metric,
216 target_value, reward_points, created_at, updated_at
217 FROM challenges
218 WHERE organization_id = $1
219 AND status = 'Active'
220 AND start_date <= NOW()
221 AND end_date > NOW()
222 ORDER BY start_date DESC
223 "#,
224 )
225 .bind(organization_id)
226 .fetch_all(&self.pool)
227 .await
228 .map_err(|e| format!("Failed to find active challenges: {}", e))?;
229
230 rows.iter().map(Self::row_to_challenge).collect()
231 }
232
233 async fn find_ended_not_completed(&self) -> Result<Vec<Challenge>, String> {
234 let rows = sqlx::query(
235 r#"
236 SELECT id, organization_id, building_id, challenge_type, status, title,
237 description, icon, start_date, end_date, target_metric,
238 target_value, reward_points, created_at, updated_at
239 FROM challenges
240 WHERE status = 'Active'
241 AND end_date <= NOW()
242 ORDER BY end_date ASC
243 "#,
244 )
245 .fetch_all(&self.pool)
246 .await
247 .map_err(|e| format!("Failed to find ended challenges: {}", e))?;
248
249 rows.iter().map(Self::row_to_challenge).collect()
250 }
251
252 async fn update(&self, challenge: &Challenge) -> Result<Challenge, String> {
253 let challenge_type_str = serde_json::to_string(&challenge.challenge_type)
254 .map_err(|e| format!("Failed to serialize challenge_type: {}", e))?
255 .trim_matches('"')
256 .to_string();
257
258 let status_str = serde_json::to_string(&challenge.status)
259 .map_err(|e| format!("Failed to serialize status: {}", e))?
260 .trim_matches('"')
261 .to_string();
262
263 let result = sqlx::query(
264 r#"
265 UPDATE challenges
266 SET challenge_type = $2::challenge_type,
267 status = $3::challenge_status,
268 title = $4,
269 description = $5,
270 icon = $6,
271 start_date = $7,
272 end_date = $8,
273 target_metric = $9,
274 target_value = $10,
275 reward_points = $11,
276 updated_at = $12
277 WHERE id = $1
278 "#,
279 )
280 .bind(&challenge.id)
281 .bind(&challenge_type_str)
282 .bind(&status_str)
283 .bind(&challenge.title)
284 .bind(&challenge.description)
285 .bind(&challenge.icon)
286 .bind(&challenge.start_date)
287 .bind(&challenge.end_date)
288 .bind(&challenge.target_metric)
289 .bind(&challenge.target_value)
290 .bind(&challenge.reward_points)
291 .bind(&challenge.updated_at)
292 .execute(&self.pool)
293 .await
294 .map_err(|e| format!("Failed to update challenge: {}", e))?;
295
296 if result.rows_affected() == 0 {
297 return Err("Challenge not found".to_string());
298 }
299
300 Ok(challenge.clone())
301 }
302
303 async fn delete(&self, id: Uuid) -> Result<(), String> {
304 let result = sqlx::query(
305 r#"
306 DELETE FROM challenges
307 WHERE id = $1
308 "#,
309 )
310 .bind(id)
311 .execute(&self.pool)
312 .await
313 .map_err(|e| format!("Failed to delete challenge: {}", e))?;
314
315 if result.rows_affected() == 0 {
316 return Err("Challenge not found".to_string());
317 }
318
319 Ok(())
320 }
321
322 async fn count_by_organization(&self, organization_id: Uuid) -> Result<i64, String> {
323 let row = sqlx::query(
324 r#"
325 SELECT COUNT(*) as count
326 FROM challenges
327 WHERE organization_id = $1
328 "#,
329 )
330 .bind(organization_id)
331 .fetch_one(&self.pool)
332 .await
333 .map_err(|e| format!("Failed to count challenges: {}", e))?;
334
335 let count: i64 = row
336 .try_get("count")
337 .map_err(|e| format!("Failed to get count: {}", e))?;
338
339 Ok(count)
340 }
341}
342
343pub struct PostgresChallengeProgressRepository {
348 pool: DbPool,
349}
350
351impl PostgresChallengeProgressRepository {
352 pub fn new(pool: DbPool) -> Self {
353 Self { pool }
354 }
355
356 fn row_to_progress(row: &sqlx::postgres::PgRow) -> Result<ChallengeProgress, String> {
358 Ok(ChallengeProgress {
359 id: row
360 .try_get("id")
361 .map_err(|e| format!("Failed to get id: {}", e))?,
362 challenge_id: row
363 .try_get("challenge_id")
364 .map_err(|e| format!("Failed to get challenge_id: {}", e))?,
365 user_id: row
366 .try_get("user_id")
367 .map_err(|e| format!("Failed to get user_id: {}", e))?,
368 current_value: row
369 .try_get("current_value")
370 .map_err(|e| format!("Failed to get current_value: {}", e))?,
371 completed: row
372 .try_get("completed")
373 .map_err(|e| format!("Failed to get completed: {}", e))?,
374 completed_at: row
375 .try_get("completed_at")
376 .map_err(|e| format!("Failed to get completed_at: {}", e))?,
377 created_at: row
378 .try_get("created_at")
379 .map_err(|e| format!("Failed to get created_at: {}", e))?,
380 updated_at: row
381 .try_get("updated_at")
382 .map_err(|e| format!("Failed to get updated_at: {}", e))?,
383 })
384 }
385}
386
387#[async_trait]
388impl ChallengeProgressRepository for PostgresChallengeProgressRepository {
389 async fn create(&self, progress: &ChallengeProgress) -> Result<ChallengeProgress, String> {
390 sqlx::query(
391 r#"
392 INSERT INTO challenge_progress (
393 id, challenge_id, user_id, current_value, completed,
394 completed_at, created_at, updated_at
395 )
396 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
397 "#,
398 )
399 .bind(&progress.id)
400 .bind(&progress.challenge_id)
401 .bind(&progress.user_id)
402 .bind(&progress.current_value)
403 .bind(&progress.completed)
404 .bind(&progress.completed_at)
405 .bind(&progress.created_at)
406 .bind(&progress.updated_at)
407 .execute(&self.pool)
408 .await
409 .map_err(|e| format!("Failed to create challenge progress: {}", e))?;
410
411 Ok(progress.clone())
412 }
413
414 async fn find_by_id(&self, id: Uuid) -> Result<Option<ChallengeProgress>, String> {
415 let row = sqlx::query(
416 r#"
417 SELECT id, challenge_id, user_id, current_value, completed,
418 completed_at, created_at, updated_at
419 FROM challenge_progress
420 WHERE id = $1
421 "#,
422 )
423 .bind(id)
424 .fetch_optional(&self.pool)
425 .await
426 .map_err(|e| format!("Failed to find challenge progress by id: {}", e))?;
427
428 row.as_ref().map(Self::row_to_progress).transpose()
429 }
430
431 async fn find_by_user_and_challenge(
432 &self,
433 user_id: Uuid,
434 challenge_id: Uuid,
435 ) -> Result<Option<ChallengeProgress>, String> {
436 let row = sqlx::query(
437 r#"
438 SELECT id, challenge_id, user_id, current_value, completed,
439 completed_at, created_at, updated_at
440 FROM challenge_progress
441 WHERE user_id = $1 AND challenge_id = $2
442 "#,
443 )
444 .bind(user_id)
445 .bind(challenge_id)
446 .fetch_optional(&self.pool)
447 .await
448 .map_err(|e| format!("Failed to find challenge progress: {}", e))?;
449
450 row.as_ref().map(Self::row_to_progress).transpose()
451 }
452
453 async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ChallengeProgress>, String> {
454 let rows = sqlx::query(
455 r#"
456 SELECT id, challenge_id, user_id, current_value, completed,
457 completed_at, created_at, updated_at
458 FROM challenge_progress
459 WHERE user_id = $1
460 ORDER BY created_at DESC
461 "#,
462 )
463 .bind(user_id)
464 .fetch_all(&self.pool)
465 .await
466 .map_err(|e| format!("Failed to find progress by user: {}", e))?;
467
468 rows.iter().map(Self::row_to_progress).collect()
469 }
470
471 async fn find_by_challenge(
472 &self,
473 challenge_id: Uuid,
474 ) -> Result<Vec<ChallengeProgress>, String> {
475 let rows = sqlx::query(
476 r#"
477 SELECT id, challenge_id, user_id, current_value, completed,
478 completed_at, created_at, updated_at
479 FROM challenge_progress
480 WHERE challenge_id = $1
481 ORDER BY current_value DESC
482 "#,
483 )
484 .bind(challenge_id)
485 .fetch_all(&self.pool)
486 .await
487 .map_err(|e| format!("Failed to find progress by challenge: {}", e))?;
488
489 rows.iter().map(Self::row_to_progress).collect()
490 }
491
492 async fn find_active_by_user(&self, user_id: Uuid) -> Result<Vec<ChallengeProgress>, String> {
493 let rows = sqlx::query(
494 r#"
495 SELECT cp.id, cp.challenge_id, cp.user_id, cp.current_value,
496 cp.completed, cp.completed_at, cp.created_at, cp.updated_at
497 FROM challenge_progress cp
498 JOIN challenges c ON c.id = cp.challenge_id
499 WHERE cp.user_id = $1
500 AND c.status = 'Active'
501 AND cp.completed = FALSE
502 ORDER BY c.end_date ASC
503 "#,
504 )
505 .bind(user_id)
506 .fetch_all(&self.pool)
507 .await
508 .map_err(|e| format!("Failed to find active progress: {}", e))?;
509
510 rows.iter().map(Self::row_to_progress).collect()
511 }
512
513 async fn update(&self, progress: &ChallengeProgress) -> Result<ChallengeProgress, String> {
514 let result = sqlx::query(
515 r#"
516 UPDATE challenge_progress
517 SET current_value = $2,
518 completed = $3,
519 completed_at = $4,
520 updated_at = $5
521 WHERE id = $1
522 "#,
523 )
524 .bind(&progress.id)
525 .bind(&progress.current_value)
526 .bind(&progress.completed)
527 .bind(&progress.completed_at)
528 .bind(&progress.updated_at)
529 .execute(&self.pool)
530 .await
531 .map_err(|e| format!("Failed to update challenge progress: {}", e))?;
532
533 if result.rows_affected() == 0 {
534 return Err("Challenge progress not found".to_string());
535 }
536
537 Ok(progress.clone())
538 }
539
540 async fn count_completed_by_user(&self, user_id: Uuid) -> Result<i64, String> {
541 let row = sqlx::query(
542 r#"
543 SELECT COUNT(*) as count
544 FROM challenge_progress
545 WHERE user_id = $1 AND completed = TRUE
546 "#,
547 )
548 .bind(user_id)
549 .fetch_one(&self.pool)
550 .await
551 .map_err(|e| format!("Failed to count completed challenges: {}", e))?;
552
553 let count: i64 = row
554 .try_get("count")
555 .map_err(|e| format!("Failed to get count: {}", e))?;
556
557 Ok(count)
558 }
559
560 async fn get_leaderboard(
561 &self,
562 organization_id: Uuid,
563 building_id: Option<Uuid>,
564 limit: i64,
565 ) -> Result<Vec<(Uuid, i32)>, String> {
566 let rows = if let Some(bldg_id) = building_id {
569 sqlx::query(
570 r#"
571 SELECT cp.user_id, COALESCE(SUM(c.reward_points), 0)::INTEGER as total_points
572 FROM challenge_progress cp
573 JOIN challenges c ON c.id = cp.challenge_id
574 WHERE c.organization_id = $1
575 AND c.building_id = $2
576 AND cp.completed = TRUE
577 GROUP BY cp.user_id
578 ORDER BY total_points DESC
579 LIMIT $3
580 "#,
581 )
582 .bind(organization_id)
583 .bind(bldg_id)
584 .bind(limit)
585 .fetch_all(&self.pool)
586 .await
587 .map_err(|e| format!("Failed to get leaderboard: {}", e))?
588 } else {
589 sqlx::query(
590 r#"
591 SELECT cp.user_id, COALESCE(SUM(c.reward_points), 0)::INTEGER as total_points
592 FROM challenge_progress cp
593 JOIN challenges c ON c.id = cp.challenge_id
594 WHERE c.organization_id = $1
595 AND cp.completed = TRUE
596 GROUP BY cp.user_id
597 ORDER BY total_points DESC
598 LIMIT $2
599 "#,
600 )
601 .bind(organization_id)
602 .bind(limit)
603 .fetch_all(&self.pool)
604 .await
605 .map_err(|e| format!("Failed to get leaderboard: {}", e))?
606 };
607
608 let leaderboard = rows
609 .iter()
610 .map(|row| {
611 let user_id: Uuid = row
612 .try_get("user_id")
613 .map_err(|e| format!("Failed to get user_id: {}", e))?;
614 let total_points: i32 = row
615 .try_get("total_points")
616 .map_err(|e| format!("Failed to get total_points: {}", e))?;
617 Ok((user_id, total_points))
618 })
619 .collect::<Result<Vec<_>, String>>()?;
620
621 Ok(leaderboard)
622 }
623}