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