1use crate::domain::entities::{
2 Achievement, AchievementCategory, AchievementTier, ChallengeProgress, ChallengeStatus,
3 ChallengeType, UserAchievement,
4};
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct CreateAchievementDto {
16 pub organization_id: Uuid,
17 pub category: AchievementCategory,
18 pub tier: AchievementTier,
19 pub name: String,
20 pub description: String,
21 pub icon: String,
22 pub points_value: i32,
23 pub requirements: String, pub is_secret: bool,
25 pub is_repeatable: bool,
26 pub display_order: i32,
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone)]
31pub struct UpdateAchievementDto {
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub name: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub description: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub icon: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub points_value: Option<i32>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub requirements: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub is_secret: Option<bool>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub is_repeatable: Option<bool>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub display_order: Option<i32>,
48}
49
50#[derive(Debug, Serialize, Deserialize, Clone)]
52pub struct AchievementResponseDto {
53 pub id: Uuid,
54 pub organization_id: Uuid,
55 pub category: AchievementCategory,
56 pub tier: AchievementTier,
57 pub name: String,
58 pub description: String,
59 pub icon: String,
60 pub points_value: i32,
61 pub requirements: String,
62 pub is_secret: bool,
63 pub is_repeatable: bool,
64 pub display_order: i32,
65 pub created_at: DateTime<Utc>,
66 pub updated_at: DateTime<Utc>,
67}
68
69impl From<Achievement> for AchievementResponseDto {
70 fn from(achievement: Achievement) -> Self {
71 Self {
72 id: achievement.id,
73 organization_id: achievement.organization_id,
74 category: achievement.category,
75 tier: achievement.tier,
76 name: achievement.name,
77 description: achievement.description,
78 icon: achievement.icon,
79 points_value: achievement.points_value,
80 requirements: achievement.requirements,
81 is_secret: achievement.is_secret,
82 is_repeatable: achievement.is_repeatable,
83 display_order: achievement.display_order,
84 created_at: achievement.created_at,
85 updated_at: achievement.updated_at,
86 }
87 }
88}
89
90#[derive(Debug, Serialize, Deserialize, Clone)]
96pub struct UserAchievementResponseDto {
97 pub id: Uuid,
98 pub user_id: Uuid,
99 pub achievement_id: Uuid,
100 pub earned_at: DateTime<Utc>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub progress_data: Option<String>,
103 pub times_earned: i32,
104 pub achievement: AchievementResponseDto,
106}
107
108impl UserAchievementResponseDto {
109 pub fn from_entities(user_achievement: UserAchievement, achievement: Achievement) -> Self {
110 Self {
111 id: user_achievement.id,
112 user_id: user_achievement.user_id,
113 achievement_id: user_achievement.achievement_id,
114 earned_at: user_achievement.earned_at,
115 progress_data: user_achievement.progress_data,
116 times_earned: user_achievement.times_earned,
117 achievement: AchievementResponseDto::from(achievement),
118 }
119 }
120}
121
122#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct CreateChallengeDto {
129 pub organization_id: Uuid,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub building_id: Option<Uuid>,
132 pub challenge_type: ChallengeType,
133 pub title: String,
134 pub description: String,
135 pub icon: String,
136 pub start_date: DateTime<Utc>,
137 pub end_date: DateTime<Utc>,
138 pub target_metric: String,
139 pub target_value: i32,
140 pub reward_points: i32,
141}
142
143#[derive(Debug, Serialize, Deserialize, Clone)]
145pub struct UpdateChallengeDto {
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub title: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub description: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub icon: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub start_date: Option<DateTime<Utc>>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub end_date: Option<DateTime<Utc>>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub target_value: Option<i32>,
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub reward_points: Option<i32>,
160}
161
162#[derive(Debug, Serialize, Deserialize, Clone)]
164pub struct ChallengeResponseDto {
165 pub id: Uuid,
166 pub organization_id: Uuid,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub building_id: Option<Uuid>,
169 pub challenge_type: ChallengeType,
170 pub status: ChallengeStatus,
171 pub title: String,
172 pub description: String,
173 pub icon: String,
174 pub start_date: DateTime<Utc>,
175 pub end_date: DateTime<Utc>,
176 pub target_metric: String,
177 pub target_value: i32,
178 pub reward_points: i32,
179 pub created_at: DateTime<Utc>,
180 pub updated_at: DateTime<Utc>,
181 pub duration_days: i64,
183 pub is_currently_active: bool,
184 pub has_ended: bool,
185}
186
187impl From<crate::domain::entities::Challenge> for ChallengeResponseDto {
188 fn from(challenge: crate::domain::entities::Challenge) -> Self {
189 Self {
190 id: challenge.id,
191 organization_id: challenge.organization_id,
192 building_id: challenge.building_id,
193 challenge_type: challenge.challenge_type.clone(),
194 status: challenge.status.clone(),
195 title: challenge.title.clone(),
196 description: challenge.description.clone(),
197 icon: challenge.icon.clone(),
198 start_date: challenge.start_date,
199 end_date: challenge.end_date,
200 target_metric: challenge.target_metric.clone(),
201 target_value: challenge.target_value,
202 reward_points: challenge.reward_points,
203 created_at: challenge.created_at,
204 updated_at: challenge.updated_at,
205 duration_days: challenge.duration_days(),
207 is_currently_active: challenge.is_currently_active(),
208 has_ended: challenge.has_ended(),
209 }
210 }
211}
212
213#[derive(Debug, Serialize, Deserialize, Clone)]
219pub struct ChallengeProgressResponseDto {
220 pub id: Uuid,
221 pub challenge_id: Uuid,
222 pub user_id: Uuid,
223 pub current_value: i32,
224 pub completed: bool,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub completed_at: Option<DateTime<Utc>>,
227 pub created_at: DateTime<Utc>,
228 pub updated_at: DateTime<Utc>,
229 pub challenge: ChallengeResponseDto,
231 pub completion_percentage: f64,
233}
234
235impl ChallengeProgressResponseDto {
236 pub fn from_entities(
237 progress: ChallengeProgress,
238 challenge: crate::domain::entities::Challenge,
239 ) -> Self {
240 let completion_percentage = progress.completion_percentage(challenge.target_value);
241 Self {
242 id: progress.id,
243 challenge_id: progress.challenge_id,
244 user_id: progress.user_id,
245 current_value: progress.current_value,
246 completed: progress.completed,
247 completed_at: progress.completed_at,
248 created_at: progress.created_at,
249 updated_at: progress.updated_at,
250 challenge: ChallengeResponseDto::from(challenge),
251 completion_percentage,
252 }
253 }
254}
255
256#[derive(Debug, Serialize, Deserialize, Clone)]
262pub struct LeaderboardEntryDto {
263 pub user_id: Uuid,
264 pub username: String, pub total_points: i32,
266 pub achievements_count: i32,
267 pub challenges_completed: i32,
268 pub rank: i32,
269}
270
271#[derive(Debug, Serialize, Deserialize, Clone)]
273pub struct LeaderboardResponseDto {
274 pub organization_id: Uuid,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub building_id: Option<Uuid>,
277 pub entries: Vec<LeaderboardEntryDto>,
278 pub total_users: i32,
279}
280
281#[derive(Debug, Serialize, Deserialize, Clone)]
283pub struct UserGamificationStatsDto {
284 pub user_id: Uuid,
285 pub total_points: i32,
286 pub achievements_earned: i32,
287 pub achievements_available: i32,
288 pub challenges_completed: i32,
289 pub challenges_active: i32,
290 pub rank: Option<i32>,
291 pub recent_achievements: Vec<UserAchievementResponseDto>, }
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_achievement_response_dto_from_entity() {
300 let organization_id = Uuid::new_v4();
301 let achievement = Achievement::new(
302 organization_id,
303 AchievementCategory::Community,
304 AchievementTier::Bronze,
305 "First Booking".to_string(),
306 "Made your first resource booking".to_string(),
307 "🎉".to_string(),
308 10,
309 r#"{"action": "booking_created", "count": 1}"#.to_string(),
310 false,
311 false,
312 1,
313 )
314 .unwrap();
315
316 let dto = AchievementResponseDto::from(achievement.clone());
317
318 assert_eq!(dto.id, achievement.id);
319 assert_eq!(dto.name, "First Booking");
320 assert_eq!(dto.category, AchievementCategory::Community);
321 assert_eq!(dto.tier, AchievementTier::Bronze);
322 assert_eq!(dto.points_value, 10);
323 }
324
325 #[test]
326 fn test_challenge_response_dto_computed_fields() {
327 let organization_id = Uuid::new_v4();
328 let start_date = Utc::now() + chrono::Duration::days(1);
329 let end_date = start_date + chrono::Duration::days(7);
330
331 let challenge = crate::domain::entities::Challenge::new(
332 organization_id,
333 None,
334 ChallengeType::Individual,
335 "Booking Week".to_string(),
336 "Make 5 resource bookings this week to earn points!".to_string(),
337 "📅".to_string(),
338 start_date,
339 end_date,
340 "bookings_created".to_string(),
341 5,
342 50,
343 )
344 .unwrap();
345
346 let dto = ChallengeResponseDto::from(challenge);
347
348 assert_eq!(dto.duration_days, 7);
349 assert!(!dto.is_currently_active); assert!(!dto.has_ended);
351 }
352
353 #[test]
354 fn test_challenge_progress_completion_percentage() {
355 let organization_id = Uuid::new_v4();
356 let start_date = Utc::now() + chrono::Duration::days(1);
357 let end_date = start_date + chrono::Duration::days(7);
358
359 let challenge = crate::domain::entities::Challenge::new(
360 organization_id,
361 None,
362 ChallengeType::Individual,
363 "Booking Week".to_string(),
364 "Make 5 resource bookings this week to earn points!".to_string(),
365 "📅".to_string(),
366 start_date,
367 end_date,
368 "bookings_created".to_string(),
369 10,
370 50,
371 )
372 .unwrap();
373
374 let challenge_id = challenge.id;
375 let user_id = Uuid::new_v4();
376 let mut progress = ChallengeProgress::new(challenge_id, user_id);
377 progress.increment(3).unwrap();
378
379 let dto = ChallengeProgressResponseDto::from_entities(progress, challenge);
380
381 assert_eq!(dto.completion_percentage, 30.0);
382 assert!(!dto.completed);
383 }
384}