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