1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, utoipa::ToSchema)]
7pub enum AchievementCategory {
8 Community, Sel, Booking, Sharing, Skills, Notice, Governance, Milestone, }
17
18#[derive(
20 Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, utoipa::ToSchema,
21)]
22pub enum AchievementTier {
23 Bronze, Silver, Gold, Platinum, Diamond, }
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Achievement {
48 pub id: Uuid,
49 pub organization_id: Uuid,
50 pub category: AchievementCategory,
51 pub tier: AchievementTier,
52 pub name: String, pub description: String, pub icon: String, pub points_value: i32, pub requirements: String, pub is_secret: bool, pub is_repeatable: bool, pub display_order: i32, pub created_at: DateTime<Utc>,
61 pub updated_at: DateTime<Utc>,
62}
63
64impl Achievement {
65 pub const MIN_NAME_LENGTH: usize = 3;
67 pub const MAX_NAME_LENGTH: usize = 100;
69 pub const MIN_DESCRIPTION_LENGTH: usize = 10;
71 pub const MAX_DESCRIPTION_LENGTH: usize = 500;
73 pub const MAX_POINTS_VALUE: i32 = 1000;
75
76 pub fn new(
85 organization_id: Uuid,
86 category: AchievementCategory,
87 tier: AchievementTier,
88 name: String,
89 description: String,
90 icon: String,
91 points_value: i32,
92 requirements: String,
93 is_secret: bool,
94 is_repeatable: bool,
95 display_order: i32,
96 ) -> Result<Self, String> {
97 if name.len() < Self::MIN_NAME_LENGTH || name.len() > Self::MAX_NAME_LENGTH {
99 return Err(format!(
100 "Achievement name must be {}-{} characters",
101 Self::MIN_NAME_LENGTH,
102 Self::MAX_NAME_LENGTH
103 ));
104 }
105
106 if description.len() < Self::MIN_DESCRIPTION_LENGTH
108 || description.len() > Self::MAX_DESCRIPTION_LENGTH
109 {
110 return Err(format!(
111 "Achievement description must be {}-{} characters",
112 Self::MIN_DESCRIPTION_LENGTH,
113 Self::MAX_DESCRIPTION_LENGTH
114 ));
115 }
116
117 if icon.trim().is_empty() {
119 return Err("Achievement icon cannot be empty".to_string());
120 }
121
122 if points_value < 0 || points_value > Self::MAX_POINTS_VALUE {
124 return Err(format!(
125 "Points value must be 0-{} points",
126 Self::MAX_POINTS_VALUE
127 ));
128 }
129
130 if requirements.trim().is_empty() {
132 return Err("Achievement requirements cannot be empty".to_string());
133 }
134
135 let now = Utc::now();
136 Ok(Self {
137 id: Uuid::new_v4(),
138 organization_id,
139 category,
140 tier,
141 name,
142 description,
143 icon,
144 points_value,
145 requirements,
146 is_secret,
147 is_repeatable,
148 display_order,
149 created_at: now,
150 updated_at: now,
151 })
152 }
153
154 pub fn update(
156 &mut self,
157 name: Option<String>,
158 description: Option<String>,
159 icon: Option<String>,
160 points_value: Option<i32>,
161 requirements: Option<String>,
162 is_secret: Option<bool>,
163 is_repeatable: Option<bool>,
164 display_order: Option<i32>,
165 ) -> Result<(), String> {
166 if let Some(n) = name {
168 if n.len() < Self::MIN_NAME_LENGTH || n.len() > Self::MAX_NAME_LENGTH {
169 return Err(format!(
170 "Achievement name must be {}-{} characters",
171 Self::MIN_NAME_LENGTH,
172 Self::MAX_NAME_LENGTH
173 ));
174 }
175 self.name = n;
176 }
177
178 if let Some(d) = description {
180 if d.len() < Self::MIN_DESCRIPTION_LENGTH || d.len() > Self::MAX_DESCRIPTION_LENGTH {
181 return Err(format!(
182 "Achievement description must be {}-{} characters",
183 Self::MIN_DESCRIPTION_LENGTH,
184 Self::MAX_DESCRIPTION_LENGTH
185 ));
186 }
187 self.description = d;
188 }
189
190 if let Some(i) = icon {
192 if i.trim().is_empty() {
193 return Err("Achievement icon cannot be empty".to_string());
194 }
195 self.icon = i;
196 }
197
198 if let Some(p) = points_value {
200 if p < 0 || p > Self::MAX_POINTS_VALUE {
201 return Err(format!(
202 "Points value must be 0-{} points",
203 Self::MAX_POINTS_VALUE
204 ));
205 }
206 self.points_value = p;
207 }
208
209 if let Some(r) = requirements {
211 if r.trim().is_empty() {
212 return Err("Achievement requirements cannot be empty".to_string());
213 }
214 self.requirements = r;
215 }
216
217 if let Some(s) = is_secret {
219 self.is_secret = s;
220 }
221 if let Some(r) = is_repeatable {
222 self.is_repeatable = r;
223 }
224 if let Some(o) = display_order {
225 self.display_order = o;
226 }
227
228 self.updated_at = Utc::now();
229 Ok(())
230 }
231
232 pub fn default_points_for_tier(tier: &AchievementTier) -> i32 {
234 match tier {
235 AchievementTier::Bronze => 10,
236 AchievementTier::Silver => 25,
237 AchievementTier::Gold => 50,
238 AchievementTier::Platinum => 100,
239 AchievementTier::Diamond => 250,
240 }
241 }
242
243 pub fn update_name(&mut self, name: String) -> Result<(), String> {
245 self.update(Some(name), None, None, None, None, None, None, None)
246 }
247
248 pub fn update_description(&mut self, description: String) -> Result<(), String> {
250 self.update(None, Some(description), None, None, None, None, None, None)
251 }
252
253 pub fn update_icon(&mut self, icon: String) -> Result<(), String> {
255 self.update(None, None, Some(icon), None, None, None, None, None)
256 }
257
258 pub fn update_points_value(&mut self, points_value: i32) -> Result<(), String> {
260 self.update(None, None, None, Some(points_value), None, None, None, None)
261 }
262
263 pub fn update_requirements(&mut self, requirements: String) -> Result<(), String> {
265 self.update(None, None, None, None, Some(requirements), None, None, None)
266 }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct UserAchievement {
272 pub id: Uuid,
273 pub user_id: Uuid,
274 pub achievement_id: Uuid,
275 pub earned_at: DateTime<Utc>,
276 pub progress_data: Option<String>, pub times_earned: i32, }
279
280impl UserAchievement {
281 pub fn new(user_id: Uuid, achievement_id: Uuid, progress_data: Option<String>) -> Self {
283 Self {
284 id: Uuid::new_v4(),
285 user_id,
286 achievement_id,
287 earned_at: Utc::now(),
288 progress_data,
289 times_earned: 1,
290 }
291 }
292
293 pub fn increment_earned(&mut self) {
295 self.times_earned += 1;
296 }
297
298 pub fn repeat_earn(&mut self) -> Result<(), String> {
300 self.increment_earned();
301 Ok(())
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn create_test_achievement() -> Achievement {
310 let organization_id = Uuid::new_v4();
311 Achievement::new(
312 organization_id,
313 AchievementCategory::Community,
314 AchievementTier::Bronze,
315 "First Booking".to_string(),
316 "Made your first resource booking".to_string(),
317 "π".to_string(),
318 10,
319 r#"{"action": "booking_created", "count": 1}"#.to_string(),
320 false,
321 false,
322 1,
323 )
324 .unwrap()
325 }
326
327 #[test]
328 fn test_create_achievement_success() {
329 let achievement = create_test_achievement();
330 assert_eq!(achievement.name, "First Booking");
331 assert_eq!(achievement.category, AchievementCategory::Community);
332 assert_eq!(achievement.tier, AchievementTier::Bronze);
333 assert_eq!(achievement.points_value, 10);
334 assert!(!achievement.is_secret);
335 assert!(!achievement.is_repeatable);
336 }
337
338 #[test]
339 fn test_create_achievement_invalid_name() {
340 let organization_id = Uuid::new_v4();
341 let result = Achievement::new(
342 organization_id,
343 AchievementCategory::Community,
344 AchievementTier::Bronze,
345 "AB".to_string(), "Made your first resource booking".to_string(),
347 "π".to_string(),
348 10,
349 r#"{"action": "booking_created", "count": 1}"#.to_string(),
350 false,
351 false,
352 1,
353 );
354
355 assert!(result.is_err());
356 assert!(result.unwrap_err().contains("Achievement name must be"));
357 }
358
359 #[test]
360 fn test_create_achievement_invalid_description() {
361 let organization_id = Uuid::new_v4();
362 let result = Achievement::new(
363 organization_id,
364 AchievementCategory::Community,
365 AchievementTier::Bronze,
366 "First Booking".to_string(),
367 "Short".to_string(), "π".to_string(),
369 10,
370 r#"{"action": "booking_created", "count": 1}"#.to_string(),
371 false,
372 false,
373 1,
374 );
375
376 assert!(result.is_err());
377 assert!(result
378 .unwrap_err()
379 .contains("Achievement description must be"));
380 }
381
382 #[test]
383 fn test_create_achievement_invalid_icon() {
384 let organization_id = Uuid::new_v4();
385 let result = Achievement::new(
386 organization_id,
387 AchievementCategory::Community,
388 AchievementTier::Bronze,
389 "First Booking".to_string(),
390 "Made your first resource booking".to_string(),
391 "".to_string(), 10,
393 r#"{"action": "booking_created", "count": 1}"#.to_string(),
394 false,
395 false,
396 1,
397 );
398
399 assert!(result.is_err());
400 assert!(result
401 .unwrap_err()
402 .contains("Achievement icon cannot be empty"));
403 }
404
405 #[test]
406 fn test_create_achievement_invalid_points() {
407 let organization_id = Uuid::new_v4();
408 let result = Achievement::new(
409 organization_id,
410 AchievementCategory::Community,
411 AchievementTier::Bronze,
412 "First Booking".to_string(),
413 "Made your first resource booking".to_string(),
414 "π".to_string(),
415 2000, r#"{"action": "booking_created", "count": 1}"#.to_string(),
417 false,
418 false,
419 1,
420 );
421
422 assert!(result.is_err());
423 assert!(result.unwrap_err().contains("Points value must be"));
424 }
425
426 #[test]
427 fn test_create_achievement_invalid_requirements() {
428 let organization_id = Uuid::new_v4();
429 let result = Achievement::new(
430 organization_id,
431 AchievementCategory::Community,
432 AchievementTier::Bronze,
433 "First Booking".to_string(),
434 "Made your first resource booking".to_string(),
435 "π".to_string(),
436 10,
437 "".to_string(), false,
439 false,
440 1,
441 );
442
443 assert!(result.is_err());
444 assert!(result
445 .unwrap_err()
446 .contains("Achievement requirements cannot be empty"));
447 }
448
449 #[test]
450 fn test_update_achievement_success() {
451 let mut achievement = create_test_achievement();
452 let result = achievement.update(
453 Some("Updated Name".to_string()),
454 Some("Updated description for this achievement".to_string()),
455 Some("π".to_string()),
456 Some(25),
457 None,
458 None,
459 None,
460 Some(10),
461 );
462
463 assert!(result.is_ok());
464 assert_eq!(achievement.name, "Updated Name");
465 assert_eq!(achievement.icon, "π");
466 assert_eq!(achievement.points_value, 25);
467 assert_eq!(achievement.display_order, 10);
468 }
469
470 #[test]
471 fn test_update_achievement_invalid_name() {
472 let mut achievement = create_test_achievement();
473 let result = achievement.update(
474 Some("AB".to_string()), None,
476 None,
477 None,
478 None,
479 None,
480 None,
481 None,
482 );
483
484 assert!(result.is_err());
485 assert!(result.unwrap_err().contains("Achievement name must be"));
486 }
487
488 #[test]
489 fn test_default_points_for_tier() {
490 assert_eq!(
491 Achievement::default_points_for_tier(&AchievementTier::Bronze),
492 10
493 );
494 assert_eq!(
495 Achievement::default_points_for_tier(&AchievementTier::Silver),
496 25
497 );
498 assert_eq!(
499 Achievement::default_points_for_tier(&AchievementTier::Gold),
500 50
501 );
502 assert_eq!(
503 Achievement::default_points_for_tier(&AchievementTier::Platinum),
504 100
505 );
506 assert_eq!(
507 Achievement::default_points_for_tier(&AchievementTier::Diamond),
508 250
509 );
510 }
511
512 #[test]
513 fn test_user_achievement_new() {
514 let user_id = Uuid::new_v4();
515 let achievement_id = Uuid::new_v4();
516 let user_achievement = UserAchievement::new(user_id, achievement_id, None);
517
518 assert_eq!(user_achievement.user_id, user_id);
519 assert_eq!(user_achievement.achievement_id, achievement_id);
520 assert_eq!(user_achievement.times_earned, 1);
521 assert!(user_achievement.progress_data.is_none());
522 }
523
524 #[test]
525 fn test_user_achievement_increment() {
526 let user_id = Uuid::new_v4();
527 let achievement_id = Uuid::new_v4();
528 let mut user_achievement = UserAchievement::new(user_id, achievement_id, None);
529
530 user_achievement.increment_earned();
531 assert_eq!(user_achievement.times_earned, 2);
532
533 user_achievement.increment_earned();
534 assert_eq!(user_achievement.times_earned, 3);
535 }
536
537 #[test]
538 fn test_achievement_tier_ordering() {
539 assert!(AchievementTier::Bronze < AchievementTier::Silver);
540 assert!(AchievementTier::Silver < AchievementTier::Gold);
541 assert!(AchievementTier::Gold < AchievementTier::Platinum);
542 assert!(AchievementTier::Platinum < AchievementTier::Diamond);
543 }
544}