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