1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, utoipa::ToSchema)]
7pub enum SkillCategory {
8 HomeRepair,
10 Languages,
12 Technology,
14 Education,
16 Arts,
18 Sports,
20 Cooking,
22 Gardening,
24 Health,
26 Legal,
28 Financial,
30 PetCare,
32 Other,
34}
35
36#[derive(
38 Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, utoipa::ToSchema,
39)]
40pub enum ExpertiseLevel {
41 Beginner,
43 Intermediate,
45 Advanced,
47 Expert,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Skill {
64 pub id: Uuid,
65 pub owner_id: Uuid,
66 pub building_id: Uuid,
67 pub skill_category: SkillCategory,
68 pub skill_name: String,
69 pub expertise_level: ExpertiseLevel,
70 pub description: String,
71 pub is_available_for_help: bool,
72 pub hourly_rate_credits: Option<i32>,
74 pub years_of_experience: Option<i32>,
76 pub certifications: Option<String>,
78 pub created_at: DateTime<Utc>,
79 pub updated_at: DateTime<Utc>,
80}
81
82impl Skill {
83 pub fn new(
91 owner_id: Uuid,
92 building_id: Uuid,
93 skill_category: SkillCategory,
94 skill_name: String,
95 expertise_level: ExpertiseLevel,
96 description: String,
97 is_available_for_help: bool,
98 hourly_rate_credits: Option<i32>,
99 years_of_experience: Option<i32>,
100 certifications: Option<String>,
101 ) -> Result<Self, String> {
102 if skill_name.len() < 3 {
104 return Err("Skill name must be at least 3 characters".to_string());
105 }
106 if skill_name.len() > 100 {
107 return Err("Skill name cannot exceed 100 characters".to_string());
108 }
109
110 if description.trim().is_empty() {
112 return Err("Description cannot be empty".to_string());
113 }
114 if description.len() > 1000 {
115 return Err("Description cannot exceed 1000 characters".to_string());
116 }
117
118 if let Some(rate) = hourly_rate_credits {
120 if rate < 0 {
121 return Err("Hourly rate cannot be negative".to_string());
122 }
123 if rate > 100 {
124 return Err("Hourly rate cannot exceed 100 credits".to_string());
125 }
126 }
127
128 if let Some(years) = years_of_experience {
130 if years < 0 {
131 return Err("Years of experience cannot be negative".to_string());
132 }
133 if years > 50 {
134 return Err("Years of experience cannot exceed 50".to_string());
135 }
136 }
137
138 let now = Utc::now();
139
140 Ok(Self {
141 id: Uuid::new_v4(),
142 owner_id,
143 building_id,
144 skill_category,
145 skill_name,
146 expertise_level,
147 description,
148 is_available_for_help,
149 hourly_rate_credits,
150 years_of_experience,
151 certifications,
152 created_at: now,
153 updated_at: now,
154 })
155 }
156
157 pub fn update(
162 &mut self,
163 skill_name: Option<String>,
164 expertise_level: Option<ExpertiseLevel>,
165 description: Option<String>,
166 is_available_for_help: Option<bool>,
167 hourly_rate_credits: Option<Option<i32>>,
168 years_of_experience: Option<Option<i32>>,
169 certifications: Option<Option<String>>,
170 ) -> Result<(), String> {
171 if let Some(name) = skill_name {
173 if name.len() < 3 {
174 return Err("Skill name must be at least 3 characters".to_string());
175 }
176 if name.len() > 100 {
177 return Err("Skill name cannot exceed 100 characters".to_string());
178 }
179 self.skill_name = name;
180 }
181
182 if let Some(level) = expertise_level {
184 self.expertise_level = level;
185 }
186
187 if let Some(desc) = description {
189 if desc.trim().is_empty() {
190 return Err("Description cannot be empty".to_string());
191 }
192 if desc.len() > 1000 {
193 return Err("Description cannot exceed 1000 characters".to_string());
194 }
195 self.description = desc;
196 }
197
198 if let Some(available) = is_available_for_help {
200 self.is_available_for_help = available;
201 }
202
203 if let Some(rate_opt) = hourly_rate_credits {
205 if let Some(rate) = rate_opt {
206 if rate < 0 {
207 return Err("Hourly rate cannot be negative".to_string());
208 }
209 if rate > 100 {
210 return Err("Hourly rate cannot exceed 100 credits".to_string());
211 }
212 }
213 self.hourly_rate_credits = rate_opt;
214 }
215
216 if let Some(years_opt) = years_of_experience {
218 if let Some(years) = years_opt {
219 if years < 0 {
220 return Err("Years of experience cannot be negative".to_string());
221 }
222 if years > 50 {
223 return Err("Years of experience cannot exceed 50".to_string());
224 }
225 }
226 self.years_of_experience = years_opt;
227 }
228
229 if let Some(cert_opt) = certifications {
231 self.certifications = cert_opt;
232 }
233
234 self.updated_at = Utc::now();
235 Ok(())
236 }
237
238 pub fn mark_available(&mut self) {
240 self.is_available_for_help = true;
241 self.updated_at = Utc::now();
242 }
243
244 pub fn mark_unavailable(&mut self) {
246 self.is_available_for_help = false;
247 self.updated_at = Utc::now();
248 }
249
250 pub fn is_free(&self) -> bool {
252 self.hourly_rate_credits.is_none() || self.hourly_rate_credits == Some(0)
253 }
254
255 pub fn is_professional(&self) -> bool {
257 self.expertise_level == ExpertiseLevel::Expert || self.certifications.is_some()
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_create_skill_success() {
267 let owner_id = Uuid::new_v4();
268 let building_id = Uuid::new_v4();
269
270 let skill = Skill::new(
271 owner_id,
272 building_id,
273 SkillCategory::Technology,
274 "Web Development".to_string(),
275 ExpertiseLevel::Advanced,
276 "Full-stack web development with React and Node.js".to_string(),
277 true,
278 Some(10), Some(5), Some("Certified Web Developer".to_string()),
281 );
282
283 assert!(skill.is_ok());
284 let skill = skill.unwrap();
285 assert_eq!(skill.owner_id, owner_id);
286 assert_eq!(skill.building_id, building_id);
287 assert_eq!(skill.skill_category, SkillCategory::Technology);
288 assert!(skill.is_available_for_help);
289 assert_eq!(skill.hourly_rate_credits, Some(10));
290 }
291
292 #[test]
293 fn test_skill_name_too_short_fails() {
294 let owner_id = Uuid::new_v4();
295 let building_id = Uuid::new_v4();
296
297 let result = Skill::new(
298 owner_id,
299 building_id,
300 SkillCategory::HomeRepair,
301 "IT".to_string(), ExpertiseLevel::Beginner,
303 "Basic IT support".to_string(),
304 true,
305 None,
306 None,
307 None,
308 );
309
310 assert!(result.is_err());
311 assert_eq!(
312 result.unwrap_err(),
313 "Skill name must be at least 3 characters"
314 );
315 }
316
317 #[test]
318 fn test_skill_name_too_long_fails() {
319 let owner_id = Uuid::new_v4();
320 let building_id = Uuid::new_v4();
321 let long_name = "A".repeat(101); let result = Skill::new(
324 owner_id,
325 building_id,
326 SkillCategory::HomeRepair,
327 long_name,
328 ExpertiseLevel::Beginner,
329 "Description".to_string(),
330 true,
331 None,
332 None,
333 None,
334 );
335
336 assert!(result.is_err());
337 assert_eq!(
338 result.unwrap_err(),
339 "Skill name cannot exceed 100 characters"
340 );
341 }
342
343 #[test]
344 fn test_empty_description_fails() {
345 let owner_id = Uuid::new_v4();
346 let building_id = Uuid::new_v4();
347
348 let result = Skill::new(
349 owner_id,
350 building_id,
351 SkillCategory::Cooking,
352 "Baking".to_string(),
353 ExpertiseLevel::Intermediate,
354 " ".to_string(), true,
356 None,
357 None,
358 None,
359 );
360
361 assert!(result.is_err());
362 assert_eq!(result.unwrap_err(), "Description cannot be empty");
363 }
364
365 #[test]
366 fn test_hourly_rate_negative_fails() {
367 let owner_id = Uuid::new_v4();
368 let building_id = Uuid::new_v4();
369
370 let result = Skill::new(
371 owner_id,
372 building_id,
373 SkillCategory::Languages,
374 "English".to_string(),
375 ExpertiseLevel::Expert,
376 "English conversation and grammar".to_string(),
377 true,
378 Some(-5), None,
380 None,
381 );
382
383 assert!(result.is_err());
384 assert_eq!(result.unwrap_err(), "Hourly rate cannot be negative");
385 }
386
387 #[test]
388 fn test_hourly_rate_exceeds_100_fails() {
389 let owner_id = Uuid::new_v4();
390 let building_id = Uuid::new_v4();
391
392 let result = Skill::new(
393 owner_id,
394 building_id,
395 SkillCategory::Legal,
396 "Tax Consulting".to_string(),
397 ExpertiseLevel::Expert,
398 "Professional tax preparation".to_string(),
399 true,
400 Some(150), None,
402 None,
403 );
404
405 assert!(result.is_err());
406 assert_eq!(result.unwrap_err(), "Hourly rate cannot exceed 100 credits");
407 }
408
409 #[test]
410 fn test_years_of_experience_negative_fails() {
411 let owner_id = Uuid::new_v4();
412 let building_id = Uuid::new_v4();
413
414 let result = Skill::new(
415 owner_id,
416 building_id,
417 SkillCategory::Sports,
418 "Yoga".to_string(),
419 ExpertiseLevel::Beginner,
420 "Hatha yoga for beginners".to_string(),
421 true,
422 None,
423 Some(-2), None,
425 );
426
427 assert!(result.is_err());
428 assert_eq!(
429 result.unwrap_err(),
430 "Years of experience cannot be negative"
431 );
432 }
433
434 #[test]
435 fn test_update_skill_success() {
436 let owner_id = Uuid::new_v4();
437 let building_id = Uuid::new_v4();
438
439 let mut skill = Skill::new(
440 owner_id,
441 building_id,
442 SkillCategory::Gardening,
443 "Gardening".to_string(),
444 ExpertiseLevel::Beginner,
445 "Basic gardening and plant care".to_string(),
446 true,
447 None,
448 Some(1),
449 None,
450 )
451 .unwrap();
452
453 let result = skill.update(
454 Some("Advanced Gardening".to_string()),
455 Some(ExpertiseLevel::Intermediate),
456 Some("Organic gardening and permaculture design".to_string()),
457 Some(true),
458 Some(Some(5)), Some(Some(3)), Some(Some("Permaculture Design Certificate".to_string())),
461 );
462
463 assert!(result.is_ok());
464 assert_eq!(skill.skill_name, "Advanced Gardening");
465 assert_eq!(skill.expertise_level, ExpertiseLevel::Intermediate);
466 assert_eq!(skill.hourly_rate_credits, Some(5));
467 assert_eq!(skill.years_of_experience, Some(3));
468 }
469
470 #[test]
471 fn test_mark_available() {
472 let owner_id = Uuid::new_v4();
473 let building_id = Uuid::new_v4();
474
475 let mut skill = Skill::new(
476 owner_id,
477 building_id,
478 SkillCategory::Arts,
479 "Painting".to_string(),
480 ExpertiseLevel::Advanced,
481 "Oil and watercolor painting".to_string(),
482 false, None,
484 None,
485 None,
486 )
487 .unwrap();
488
489 assert!(!skill.is_available_for_help);
490
491 skill.mark_available();
492 assert!(skill.is_available_for_help);
493 }
494
495 #[test]
496 fn test_mark_unavailable() {
497 let owner_id = Uuid::new_v4();
498 let building_id = Uuid::new_v4();
499
500 let mut skill = Skill::new(
501 owner_id,
502 building_id,
503 SkillCategory::PetCare,
504 "Dog Training".to_string(),
505 ExpertiseLevel::Expert,
506 "Professional dog training and behavior modification".to_string(),
507 true, Some(15),
509 Some(10),
510 Some("Certified Dog Trainer".to_string()),
511 )
512 .unwrap();
513
514 assert!(skill.is_available_for_help);
515
516 skill.mark_unavailable();
517 assert!(!skill.is_available_for_help);
518 }
519
520 #[test]
521 fn test_is_free() {
522 let owner_id = Uuid::new_v4();
523 let building_id = Uuid::new_v4();
524
525 let skill1 = Skill::new(
527 owner_id,
528 building_id,
529 SkillCategory::Education,
530 "Math Tutoring".to_string(),
531 ExpertiseLevel::Advanced,
532 "High school math tutoring".to_string(),
533 true,
534 None, None,
536 None,
537 )
538 .unwrap();
539 assert!(skill1.is_free());
540
541 let skill2 = Skill::new(
543 owner_id,
544 building_id,
545 SkillCategory::Education,
546 "Math Tutoring".to_string(),
547 ExpertiseLevel::Advanced,
548 "High school math tutoring".to_string(),
549 true,
550 Some(0), None,
552 None,
553 )
554 .unwrap();
555 assert!(skill2.is_free());
556
557 let skill3 = Skill::new(
559 owner_id,
560 building_id,
561 SkillCategory::Technology,
562 "IT Support".to_string(),
563 ExpertiseLevel::Expert,
564 "Professional IT support".to_string(),
565 true,
566 Some(20), None,
568 None,
569 )
570 .unwrap();
571 assert!(!skill3.is_free());
572 }
573
574 #[test]
575 fn test_is_professional() {
576 let owner_id = Uuid::new_v4();
577 let building_id = Uuid::new_v4();
578
579 let skill1 = Skill::new(
581 owner_id,
582 building_id,
583 SkillCategory::Health,
584 "Massage Therapy".to_string(),
585 ExpertiseLevel::Expert,
586 "Therapeutic massage".to_string(),
587 true,
588 Some(30),
589 Some(15),
590 None,
591 )
592 .unwrap();
593 assert!(skill1.is_professional());
594
595 let skill2 = Skill::new(
597 owner_id,
598 building_id,
599 SkillCategory::Financial,
600 "Accounting".to_string(),
601 ExpertiseLevel::Advanced,
602 "Small business accounting".to_string(),
603 true,
604 Some(25),
605 Some(5),
606 Some("CPA License".to_string()),
607 )
608 .unwrap();
609 assert!(skill2.is_professional());
610
611 let skill3 = Skill::new(
613 owner_id,
614 building_id,
615 SkillCategory::Cooking,
616 "Baking".to_string(),
617 ExpertiseLevel::Beginner,
618 "Home baking enthusiast".to_string(),
619 true,
620 None,
621 Some(1),
622 None,
623 )
624 .unwrap();
625 assert!(!skill3.is_professional());
626 }
627}