1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum BoardPosition {
8 President, Treasurer, Member, }
12
13impl std::fmt::Display for BoardPosition {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 match self {
16 BoardPosition::President => write!(f, "president"),
17 BoardPosition::Treasurer => write!(f, "treasurer"),
18 BoardPosition::Member => write!(f, "member"),
19 }
20 }
21}
22
23impl std::str::FromStr for BoardPosition {
24 type Err = String;
25
26 fn from_str(s: &str) -> Result<Self, Self::Err> {
27 match s.to_lowercase().as_str() {
28 "president" => Ok(BoardPosition::President),
29 "treasurer" => Ok(BoardPosition::Treasurer),
30 "member" => Ok(BoardPosition::Member),
31 _ => Err(format!("Invalid board position: {}", s)),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BoardMember {
41 pub id: Uuid,
42 pub owner_id: Uuid, pub building_id: Uuid,
44 pub position: BoardPosition,
45 pub mandate_start: DateTime<Utc>,
46 pub mandate_end: DateTime<Utc>,
47 pub elected_by_meeting_id: Uuid, pub created_at: DateTime<Utc>,
49 pub updated_at: DateTime<Utc>,
50}
51
52impl BoardMember {
53 pub fn new(
56 owner_id: Uuid,
57 building_id: Uuid,
58 position: BoardPosition,
59 mandate_start: DateTime<Utc>,
60 mandate_end: DateTime<Utc>,
61 elected_by_meeting_id: Uuid,
62 ) -> Result<Self, String> {
63 if mandate_start >= mandate_end {
65 return Err("Mandate start date must be before end date".to_string());
66 }
67
68 let duration_days = (mandate_end - mandate_start).num_days();
70 if !(365..=1095).contains(&duration_days) {
71 return Err("Board member mandate cannot exceed 3 years (Art. 3.89 CC)".to_string());
72 }
73
74 let now = Utc::now();
75 Ok(Self {
76 id: Uuid::new_v4(),
77 owner_id,
78 building_id,
79 position,
80 mandate_start,
81 mandate_end,
82 elected_by_meeting_id,
83 created_at: now,
84 updated_at: now,
85 })
86 }
87
88 pub fn is_active(&self) -> bool {
90 let now = Utc::now();
91 now >= self.mandate_start && now <= self.mandate_end
92 }
93
94 pub fn days_remaining(&self) -> i64 {
97 let now = Utc::now();
98 if now > self.mandate_end {
99 return 0;
100 }
101 (self.mandate_end - now).num_days()
102 }
103
104 pub fn expires_soon(&self) -> bool {
106 self.days_remaining() > 0 && self.days_remaining() < 60
107 }
108
109 pub fn extend_mandate(
112 &mut self,
113 mandate_duration_days: i64,
114 new_elected_by_meeting_id: Uuid,
115 ) -> Result<(), String> {
116 if !self.expires_soon() && self.is_active() {
117 return Err("Cannot extend mandate more than 60 days before expiration".to_string());
118 }
119
120 if !(365..=1095).contains(&mandate_duration_days) {
122 return Err("Board member mandate cannot exceed 3 years (Art. 3.89 CC)".to_string());
123 }
124
125 self.mandate_start = self.mandate_end;
127 self.mandate_end = self.mandate_start + Duration::days(mandate_duration_days);
128 self.elected_by_meeting_id = new_elected_by_meeting_id;
129 self.updated_at = Utc::now();
130
131 Ok(())
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_create_board_member_success() {
141 let owner_id = Uuid::new_v4();
143 let building_id = Uuid::new_v4();
144 let meeting_id = Uuid::new_v4();
145 let start = Utc::now();
146 let end = start + Duration::days(365);
147
148 let result = BoardMember::new(
150 owner_id,
151 building_id,
152 BoardPosition::President,
153 start,
154 end,
155 meeting_id,
156 );
157
158 assert!(result.is_ok());
160 let member = result.unwrap();
161 assert_eq!(member.owner_id, owner_id);
162 assert_eq!(member.building_id, building_id);
163 assert_eq!(member.position, BoardPosition::President);
164 assert_eq!(member.mandate_start, start);
165 assert_eq!(member.mandate_end, end);
166 assert_eq!(member.elected_by_meeting_id, meeting_id);
167 }
168
169 #[test]
170 fn test_mandate_duration_one_year() {
171 let start = Utc::now();
173 let end = start + Duration::days(365);
174
175 let result = BoardMember::new(
177 Uuid::new_v4(),
178 Uuid::new_v4(),
179 BoardPosition::Member,
180 start,
181 end,
182 Uuid::new_v4(),
183 );
184
185 assert!(result.is_ok());
187 }
188
189 #[test]
190 fn test_mandate_duration_too_short_fails() {
191 let start = Utc::now();
193 let end = start + Duration::days(300); let result = BoardMember::new(
197 Uuid::new_v4(),
198 Uuid::new_v4(),
199 BoardPosition::Member,
200 start,
201 end,
202 Uuid::new_v4(),
203 );
204
205 assert!(result.is_err());
207 assert_eq!(
208 result.unwrap_err(),
209 "Board member mandate cannot exceed 3 years (Art. 3.89 CC)"
210 );
211 }
212
213 #[test]
214 fn test_mandate_duration_two_years() {
215 let start = Utc::now();
217 let end = start + Duration::days(730); let result = BoardMember::new(
221 Uuid::new_v4(),
222 Uuid::new_v4(),
223 BoardPosition::Member,
224 start,
225 end,
226 Uuid::new_v4(),
227 );
228
229 assert!(result.is_ok());
231 }
232
233 #[test]
234 fn test_mandate_duration_three_years() {
235 let start = Utc::now();
237 let end = start + Duration::days(1095); let result = BoardMember::new(
241 Uuid::new_v4(),
242 Uuid::new_v4(),
243 BoardPosition::Member,
244 start,
245 end,
246 Uuid::new_v4(),
247 );
248
249 assert!(result.is_ok());
251 }
252
253 #[test]
254 fn test_mandate_duration_exceeds_three_years_fails() {
255 let start = Utc::now();
257 let end = start + Duration::days(1100); let result = BoardMember::new(
261 Uuid::new_v4(),
262 Uuid::new_v4(),
263 BoardPosition::Member,
264 start,
265 end,
266 Uuid::new_v4(),
267 );
268
269 assert!(result.is_err());
271 assert_eq!(
272 result.unwrap_err(),
273 "Board member mandate cannot exceed 3 years (Art. 3.89 CC)"
274 );
275 }
276
277 #[test]
278 fn test_mandate_start_after_end_fails() {
279 let start = Utc::now();
281 let end = start - Duration::days(10); let result = BoardMember::new(
285 Uuid::new_v4(),
286 Uuid::new_v4(),
287 BoardPosition::President,
288 start,
289 end,
290 Uuid::new_v4(),
291 );
292
293 assert!(result.is_err());
295 assert_eq!(
296 result.unwrap_err(),
297 "Mandate start date must be before end date"
298 );
299 }
300
301 #[test]
302 fn test_is_active_mandate() {
303 let start = Utc::now() - Duration::days(10); let end = Utc::now() + Duration::days(355); let member = BoardMember::new(
307 Uuid::new_v4(),
308 Uuid::new_v4(),
309 BoardPosition::Member,
310 start,
311 end,
312 Uuid::new_v4(),
313 )
314 .unwrap();
315
316 assert!(member.is_active());
318 }
319
320 #[test]
321 fn test_is_not_active_future_mandate() {
322 let start = Utc::now() + Duration::days(10); let end = start + Duration::days(365);
325 let member = BoardMember::new(
326 Uuid::new_v4(),
327 Uuid::new_v4(),
328 BoardPosition::Member,
329 start,
330 end,
331 Uuid::new_v4(),
332 )
333 .unwrap();
334
335 assert!(!member.is_active());
337 }
338
339 #[test]
340 fn test_days_remaining_calculation() {
341 let start = Utc::now() - Duration::days(300); let end = start + Duration::days(365); let member = BoardMember::new(
345 Uuid::new_v4(),
346 Uuid::new_v4(),
347 BoardPosition::Treasurer,
348 start,
349 end,
350 Uuid::new_v4(),
351 )
352 .unwrap();
353
354 let remaining = member.days_remaining();
356
357 assert!((64..=66).contains(&remaining)); }
360
361 #[test]
362 fn test_days_remaining_expired_returns_zero() {
363 let start = Utc::now() - Duration::days(400); let end = start + Duration::days(365); let member = BoardMember::new(
367 Uuid::new_v4(),
368 Uuid::new_v4(),
369 BoardPosition::Member,
370 start,
371 end,
372 Uuid::new_v4(),
373 )
374 .unwrap();
375
376 let remaining = member.days_remaining();
378
379 assert_eq!(remaining, 0);
381 }
382
383 #[test]
384 fn test_expires_soon_true() {
385 let start = Utc::now() - Duration::days(320); let end = start + Duration::days(365); let member = BoardMember::new(
389 Uuid::new_v4(),
390 Uuid::new_v4(),
391 BoardPosition::President,
392 start,
393 end,
394 Uuid::new_v4(),
395 )
396 .unwrap();
397
398 assert!(member.expires_soon());
400 }
401
402 #[test]
403 fn test_expires_soon_false_far_expiration() {
404 let start = Utc::now() - Duration::days(100); let end = start + Duration::days(365); let member = BoardMember::new(
408 Uuid::new_v4(),
409 Uuid::new_v4(),
410 BoardPosition::Member,
411 start,
412 end,
413 Uuid::new_v4(),
414 )
415 .unwrap();
416
417 assert!(!member.expires_soon());
419 }
420
421 #[test]
422 fn test_extend_mandate_success() {
423 let start = Utc::now() - Duration::days(320); let end = start + Duration::days(365);
426 let new_meeting_id = Uuid::new_v4();
427 let mut member = BoardMember::new(
428 Uuid::new_v4(),
429 Uuid::new_v4(),
430 BoardPosition::President,
431 start,
432 end,
433 Uuid::new_v4(),
434 )
435 .unwrap();
436
437 let original_end = member.mandate_end;
438
439 let result = member.extend_mandate(365, new_meeting_id);
441
442 assert!(result.is_ok());
444 assert_eq!(member.mandate_start, original_end);
445 assert_eq!(member.mandate_end, original_end + Duration::days(365));
446 assert_eq!(member.elected_by_meeting_id, new_meeting_id);
447 }
448
449 #[test]
450 fn test_extend_mandate_two_years() {
451 let start = Utc::now() - Duration::days(320); let end = start + Duration::days(365);
454 let new_meeting_id = Uuid::new_v4();
455 let mut member = BoardMember::new(
456 Uuid::new_v4(),
457 Uuid::new_v4(),
458 BoardPosition::President,
459 start,
460 end,
461 Uuid::new_v4(),
462 )
463 .unwrap();
464
465 let original_end = member.mandate_end;
466
467 let result = member.extend_mandate(730, new_meeting_id);
469
470 assert!(result.is_ok());
472 assert_eq!(member.mandate_start, original_end);
473 assert_eq!(member.mandate_end, original_end + Duration::days(730));
474 }
475
476 #[test]
477 fn test_extend_mandate_exceeds_three_years_fails() {
478 let start = Utc::now() - Duration::days(320); let end = start + Duration::days(365);
481 let new_meeting_id = Uuid::new_v4();
482 let mut member = BoardMember::new(
483 Uuid::new_v4(),
484 Uuid::new_v4(),
485 BoardPosition::President,
486 start,
487 end,
488 Uuid::new_v4(),
489 )
490 .unwrap();
491
492 let result = member.extend_mandate(1100, new_meeting_id);
494
495 assert!(result.is_err());
497 assert_eq!(
498 result.unwrap_err(),
499 "Board member mandate cannot exceed 3 years (Art. 3.89 CC)"
500 );
501 }
502
503 #[test]
504 fn test_extend_mandate_fails_too_early() {
505 let start = Utc::now() - Duration::days(100); let end = start + Duration::days(365);
508 let mut member = BoardMember::new(
509 Uuid::new_v4(),
510 Uuid::new_v4(),
511 BoardPosition::Member,
512 start,
513 end,
514 Uuid::new_v4(),
515 )
516 .unwrap();
517
518 let result = member.extend_mandate(365, Uuid::new_v4());
520
521 assert!(result.is_err());
523 assert_eq!(
524 result.unwrap_err(),
525 "Cannot extend mandate more than 60 days before expiration"
526 );
527 }
528
529 #[test]
530 fn test_board_position_display() {
531 assert_eq!(BoardPosition::President.to_string(), "president");
532 assert_eq!(BoardPosition::Treasurer.to_string(), "treasurer");
533 assert_eq!(BoardPosition::Member.to_string(), "member");
534 }
535
536 #[test]
537 fn test_board_position_from_str() {
538 assert_eq!(
539 "president".parse::<BoardPosition>().unwrap(),
540 BoardPosition::President
541 );
542 assert_eq!(
543 "President".parse::<BoardPosition>().unwrap(),
544 BoardPosition::President
545 );
546 assert_eq!(
547 "TREASURER".parse::<BoardPosition>().unwrap(),
548 BoardPosition::Treasurer
549 );
550 assert_eq!(
551 "member".parse::<BoardPosition>().unwrap(),
552 BoardPosition::Member
553 );
554
555 assert!("invalid".parse::<BoardPosition>().is_err());
556 }
557}