koprogo_api/domain/entities/
board_member.rs

1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Position du membre du conseil de copropriété
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum BoardPosition {
8    President, // Président du conseil
9    Treasurer, // Trésorier du conseil
10    Member,    // Membre simple
11}
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/// Membre du conseil de copropriété (Article 577-8/4 Code Civil belge)
37/// Obligation légale pour immeubles >20 lots
38/// Les membres doivent être des copropriétaires (Owner), pas nécessairement des utilisateurs de la plateforme
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BoardMember {
41    pub id: Uuid,
42    pub owner_id: Uuid, // Référence à la table owners (copropriétaires)
43    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, // ID de l'AG qui a élu ce membre
48    pub created_at: DateTime<Utc>,
49    pub updated_at: DateTime<Utc>,
50}
51
52impl BoardMember {
53    /// Crée un nouveau membre du conseil avec validation
54    /// Validation per Art. 3.89 CC: mandate duration must be 1-3 years
55    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        // Validation: mandate_start doit être avant mandate_end
64        if mandate_start >= mandate_end {
65            return Err("Mandate start date must be before end date".to_string());
66        }
67
68        // Validation: durée du mandat doit être entre 1 et 3 ans (Art. 3.89 CC)
69        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    /// Vérifie si le mandat est actuellement actif
89    pub fn is_active(&self) -> bool {
90        let now = Utc::now();
91        now >= self.mandate_start && now <= self.mandate_end
92    }
93
94    /// Calcule le nombre de jours restants dans le mandat
95    /// Retourne 0 si le mandat est expiré
96    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    /// Vérifie si le mandat expire bientôt (< 60 jours)
105    pub fn expires_soon(&self) -> bool {
106        self.days_remaining() > 0 && self.days_remaining() < 60
107    }
108
109    /// Renouvelle le mandat pour une durée supplémentaire (Art. 3.89 CC: max 3 years)
110    /// mandate_duration_days: duration of the new mandate in days (min 365, max 1095)
111    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        // Validate mandate duration (Art. 3.89 CC: 1-3 years)
121        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        // Nouveau mandat commence à la fin de l'ancien
126        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        // Arrange
142        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        // Act
149        let result = BoardMember::new(
150            owner_id,
151            building_id,
152            BoardPosition::President,
153            start,
154            end,
155            meeting_id,
156        );
157
158        // Assert
159        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        // Arrange
172        let start = Utc::now();
173        let end = start + Duration::days(365);
174
175        // Act
176        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
186        assert!(result.is_ok());
187    }
188
189    #[test]
190    fn test_mandate_duration_too_short_fails() {
191        // Arrange
192        let start = Utc::now();
193        let end = start + Duration::days(300); // Trop court (< 365 jours, min 1 year)
194
195        // Act
196        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
206        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        // Arrange
216        let start = Utc::now();
217        let end = start + Duration::days(730); // 2 years (within 1-3 year range)
218
219        // Act
220        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
230        assert!(result.is_ok());
231    }
232
233    #[test]
234    fn test_mandate_duration_three_years() {
235        // Arrange
236        let start = Utc::now();
237        let end = start + Duration::days(1095); // Exactly 3 years
238
239        // Act
240        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
250        assert!(result.is_ok());
251    }
252
253    #[test]
254    fn test_mandate_duration_exceeds_three_years_fails() {
255        // Arrange
256        let start = Utc::now();
257        let end = start + Duration::days(1100); // Exceeds 3 years
258
259        // Act
260        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
270        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        // Arrange
280        let start = Utc::now();
281        let end = start - Duration::days(10); // End avant start
282
283        // Act
284        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
294        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        // Arrange
304        let start = Utc::now() - Duration::days(10); // Commencé il y a 10 jours
305        let end = Utc::now() + Duration::days(355); // Expire dans 355 jours
306        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        // Act & Assert
317        assert!(member.is_active());
318    }
319
320    #[test]
321    fn test_is_not_active_future_mandate() {
322        // Arrange
323        let start = Utc::now() + Duration::days(10); // Commence dans 10 jours
324        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        // Act & Assert
336        assert!(!member.is_active());
337    }
338
339    #[test]
340    fn test_days_remaining_calculation() {
341        // Arrange
342        let start = Utc::now() - Duration::days(300); // Commencé il y a 300 jours
343        let end = start + Duration::days(365); // Expire dans 65 jours
344        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        // Act
355        let remaining = member.days_remaining();
356
357        // Assert
358        assert!((64..=66).contains(&remaining)); // ±1 jour de tolérance
359    }
360
361    #[test]
362    fn test_days_remaining_expired_returns_zero() {
363        // Arrange
364        let start = Utc::now() - Duration::days(400); // Commencé il y a 400 jours
365        let end = start + Duration::days(365); // Expiré il y a 35 jours
366        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        // Act
377        let remaining = member.days_remaining();
378
379        // Assert
380        assert_eq!(remaining, 0);
381    }
382
383    #[test]
384    fn test_expires_soon_true() {
385        // Arrange
386        let start = Utc::now() - Duration::days(320); // Commencé il y a 320 jours
387        let end = start + Duration::days(365); // Expire dans 45 jours
388        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        // Act & Assert
399        assert!(member.expires_soon());
400    }
401
402    #[test]
403    fn test_expires_soon_false_far_expiration() {
404        // Arrange
405        let start = Utc::now() - Duration::days(100); // Commencé il y a 100 jours
406        let end = start + Duration::days(365); // Expire dans 265 jours
407        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        // Act & Assert
418        assert!(!member.expires_soon());
419    }
420
421    #[test]
422    fn test_extend_mandate_success() {
423        // Arrange
424        let start = Utc::now() - Duration::days(320); // Expire dans 45 jours
425        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        // Act: Extend for another 365 days
440        let result = member.extend_mandate(365, new_meeting_id);
441
442        // Assert
443        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        // Arrange
452        let start = Utc::now() - Duration::days(320); // Expire dans 45 jours
453        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        // Act: Extend for 2 years (730 days)
468        let result = member.extend_mandate(730, new_meeting_id);
469
470        // Assert
471        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        // Arrange
479        let start = Utc::now() - Duration::days(320); // Expire dans 45 jours
480        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        // Act: Try to extend for > 3 years (1100 days)
493        let result = member.extend_mandate(1100, new_meeting_id);
494
495        // Assert
496        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        // Arrange
506        let start = Utc::now() - Duration::days(100); // Expire dans 265 jours
507        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        // Act
519        let result = member.extend_mandate(365, Uuid::new_v4());
520
521        // Assert
522        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}