1use chrono::{DateTime, Utc};
2use rust_decimal::Decimal;
3use uuid::Uuid;
4
5#[derive(Debug, Clone)]
15pub struct UnitOwner {
16 pub id: Uuid,
17 pub unit_id: Uuid,
18 pub owner_id: Uuid,
19
20 pub ownership_percentage: Decimal,
23
24 pub start_date: DateTime<Utc>,
26
27 pub end_date: Option<DateTime<Utc>>,
29
30 pub is_primary_contact: bool,
32
33 pub created_at: DateTime<Utc>,
34 pub updated_at: DateTime<Utc>,
35}
36
37impl UnitOwner {
38 pub fn new(
40 unit_id: Uuid,
41 owner_id: Uuid,
42 ownership_percentage: Decimal,
43 is_primary_contact: bool,
44 ) -> Result<Self, String> {
45 if ownership_percentage <= Decimal::ZERO || ownership_percentage > Decimal::ONE {
47 return Err("Ownership percentage must be between 0 and 1".to_string());
48 }
49
50 Ok(Self {
51 id: Uuid::new_v4(),
52 unit_id,
53 owner_id,
54 ownership_percentage,
55 start_date: Utc::now(),
56 end_date: None,
57 is_primary_contact,
58 created_at: Utc::now(),
59 updated_at: Utc::now(),
60 })
61 }
62
63 pub fn new_with_start_date(
65 unit_id: Uuid,
66 owner_id: Uuid,
67 ownership_percentage: Decimal,
68 is_primary_contact: bool,
69 start_date: DateTime<Utc>,
70 ) -> Result<Self, String> {
71 if ownership_percentage <= Decimal::ZERO || ownership_percentage > Decimal::ONE {
72 return Err("Ownership percentage must be between 0 and 1".to_string());
73 }
74
75 Ok(Self {
76 id: Uuid::new_v4(),
77 unit_id,
78 owner_id,
79 ownership_percentage,
80 start_date,
81 end_date: None,
82 is_primary_contact,
83 created_at: Utc::now(),
84 updated_at: Utc::now(),
85 })
86 }
87
88 pub fn is_active(&self) -> bool {
90 self.end_date.is_none()
91 }
92
93 pub fn end_ownership(&mut self, end_date: DateTime<Utc>) -> Result<(), String> {
95 if end_date <= self.start_date {
96 return Err("End date must be after start date".to_string());
97 }
98
99 self.end_date = Some(end_date);
100 self.updated_at = Utc::now();
101 Ok(())
102 }
103
104 pub fn update_percentage(&mut self, new_percentage: Decimal) -> Result<(), String> {
106 if new_percentage <= Decimal::ZERO || new_percentage > Decimal::ONE {
107 return Err("Ownership percentage must be between 0 and 1".to_string());
108 }
109
110 self.ownership_percentage = new_percentage;
111 self.updated_at = Utc::now();
112 Ok(())
113 }
114
115 pub fn set_primary_contact(&mut self, is_primary: bool) {
117 self.is_primary_contact = is_primary;
118 self.updated_at = Utc::now();
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum OwnershipType {
136 #[default]
138 FullOwner,
139 Usufruct,
141 BareOwner,
143 Indivisaire,
145 Emphyteote,
147 Superficiaire,
149}
150
151impl OwnershipType {
152 pub fn is_full_ownership(&self) -> bool {
155 matches!(self, OwnershipType::FullOwner)
156 }
157}
158
159impl std::fmt::Display for OwnershipType {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 let s = match self {
163 OwnershipType::FullOwner => "full_owner",
164 OwnershipType::Usufruct => "usufruct",
165 OwnershipType::BareOwner => "bare_owner",
166 OwnershipType::Indivisaire => "indivisaire",
167 OwnershipType::Emphyteote => "emphyteote",
168 OwnershipType::Superficiaire => "superficiaire",
169 };
170 f.write_str(s)
171 }
172}
173
174impl std::str::FromStr for OwnershipType {
175 type Err = VotingRightError;
176
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
180 match s {
181 "full_owner" => Ok(OwnershipType::FullOwner),
182 "usufruct" => Ok(OwnershipType::Usufruct),
183 "bare_owner" => Ok(OwnershipType::BareOwner),
184 "indivisaire" => Ok(OwnershipType::Indivisaire),
185 "emphyteote" => Ok(OwnershipType::Emphyteote),
186 "superficiaire" => Ok(OwnershipType::Superficiaire),
187 other => Err(VotingRightError::UnknownOwnershipType(other.to_string())),
188 }
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum VotingRightStatus {
195 Active,
197 Suspended,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub struct LotHolder {
208 pub ownership_type: OwnershipType,
209 pub is_voting_representative: bool,
210}
211
212impl LotHolder {
213 pub fn new(ownership_type: OwnershipType, is_voting_representative: bool) -> Self {
214 Self {
215 ownership_type,
216 is_voting_representative,
217 }
218 }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
223pub enum VotingRightError {
224 UnknownOwnershipType(String),
226 MultipleRepresentatives { unit_id: Uuid, count: usize },
229}
230
231impl std::fmt::Display for VotingRightError {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 VotingRightError::UnknownOwnershipType(s) => {
235 write!(f, "Type de titularité inconnu : '{}'", s)
236 }
237 VotingRightError::MultipleRepresentatives { unit_id, count } => write!(
238 f,
239 "Lot {} : {} représentants de vote désignés, un seul autorisé (Art. 3.87 §1 CC)",
240 unit_id, count
241 ),
242 }
243 }
244}
245
246impl std::error::Error for VotingRightError {}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct VotingRightSuspendedError {
253 pub unit_id: Uuid,
254}
255
256impl std::fmt::Display for VotingRightSuspendedError {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 write!(
259 f,
260 "Droit de vote suspendu pour le lot {} : lot démembré/indivis sans \
261 représentant unique désigné (Art. 3.87 §1 CC)",
262 self.unit_id
263 )
264 }
265}
266
267impl std::error::Error for VotingRightSuspendedError {}
268
269pub fn voting_right_status(holders: &[LotHolder]) -> VotingRightStatus {
279 if holders.is_empty() {
280 return VotingRightStatus::Active;
281 }
282 if holders.iter().any(|h| h.is_voting_representative) {
283 return VotingRightStatus::Active;
284 }
285 if holders.len() == 1 && holders[0].ownership_type.is_full_ownership() {
286 return VotingRightStatus::Active;
287 }
288 VotingRightStatus::Suspended
289}
290
291pub fn assert_single_voting_representative(
295 unit_id: Uuid,
296 holders: &[LotHolder],
297) -> Result<(), VotingRightError> {
298 let count = holders
299 .iter()
300 .filter(|h| h.is_voting_representative)
301 .count();
302 if count >= 2 {
303 return Err(VotingRightError::MultipleRepresentatives { unit_id, count });
304 }
305 Ok(())
306}
307
308pub fn assert_voting_right_active(
311 unit_id: Uuid,
312 holders: &[LotHolder],
313) -> Result<(), VotingRightSuspendedError> {
314 match voting_right_status(holders) {
315 VotingRightStatus::Active => Ok(()),
316 VotingRightStatus::Suspended => Err(VotingRightSuspendedError { unit_id }),
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use rust_decimal_macros::dec;
324
325 #[test]
326 fn test_create_unit_owner() {
327 let unit_id = Uuid::new_v4();
328 let owner_id = Uuid::new_v4();
329
330 let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), true).unwrap();
331
332 assert_eq!(unit_owner.unit_id, unit_id);
333 assert_eq!(unit_owner.owner_id, owner_id);
334 assert_eq!(unit_owner.ownership_percentage, dec!(0.5));
335 assert!(unit_owner.is_primary_contact);
336 assert!(unit_owner.is_active());
337 }
338
339 #[test]
340 fn test_invalid_ownership_percentage() {
341 let unit_id = Uuid::new_v4();
342 let owner_id = Uuid::new_v4();
343
344 let result = UnitOwner::new(unit_id, owner_id, dec!(1.5), false);
346 assert!(result.is_err());
347
348 let result = UnitOwner::new(unit_id, owner_id, Decimal::ZERO, false);
350 assert!(result.is_err());
351
352 let result = UnitOwner::new(unit_id, owner_id, dec!(-0.5), false);
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_end_ownership() {
358 let unit_id = Uuid::new_v4();
359 let owner_id = Uuid::new_v4();
360
361 let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
362
363 assert!(unit_owner.is_active());
364
365 let end_date = Utc::now() + chrono::Duration::days(1);
366 unit_owner.end_ownership(end_date).unwrap();
367
368 assert!(!unit_owner.is_active());
369 assert_eq!(unit_owner.end_date, Some(end_date));
370 }
371
372 #[test]
373 fn test_invalid_end_date() {
374 let unit_id = Uuid::new_v4();
375 let owner_id = Uuid::new_v4();
376
377 let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
378
379 let invalid_end_date = unit_owner.start_date - chrono::Duration::days(1);
381 let result = unit_owner.end_ownership(invalid_end_date);
382
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn test_update_percentage() {
388 let unit_id = Uuid::new_v4();
389 let owner_id = Uuid::new_v4();
390
391 let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), true).unwrap();
392
393 unit_owner.update_percentage(dec!(0.75)).unwrap();
394 assert_eq!(unit_owner.ownership_percentage, dec!(0.75));
395
396 let result = unit_owner.update_percentage(dec!(1.5));
398 assert!(result.is_err());
399 }
400
401 #[test]
402 fn test_update_percentage_boundary_values() {
403 let unit_id = Uuid::new_v4();
404 let owner_id = Uuid::new_v4();
405
406 let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
407
408 assert!(unit_owner.update_percentage(Decimal::ONE).is_ok());
410 assert_eq!(unit_owner.ownership_percentage, Decimal::ONE);
411
412 assert!(unit_owner.update_percentage(Decimal::ZERO).is_err());
414
415 assert!(unit_owner.update_percentage(dec!(0.0001)).is_ok());
417 assert_eq!(unit_owner.ownership_percentage, dec!(0.0001));
418
419 assert!(unit_owner.update_percentage(dec!(1.0001)).is_err());
421
422 assert!(unit_owner.update_percentage(dec!(-0.5)).is_err());
424 }
425
426 #[test]
427 fn test_set_primary_contact() {
428 let unit_id = Uuid::new_v4();
429 let owner_id = Uuid::new_v4();
430
431 let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
432
433 assert!(!unit_owner.is_primary_contact);
434
435 unit_owner.set_primary_contact(true);
436 assert!(unit_owner.is_primary_contact);
437
438 unit_owner.set_primary_contact(false);
439 assert!(!unit_owner.is_primary_contact);
440 }
441
442 #[test]
443 fn test_ownership_percentage_precision() {
444 let unit_id = Uuid::new_v4();
445 let owner_id = Uuid::new_v4();
446
447 let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.3333), false).unwrap();
449 assert_eq!(unit_owner.ownership_percentage, dec!(0.3333));
450
451 let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.0001), false).unwrap();
453 assert_eq!(unit_owner.ownership_percentage, dec!(0.0001));
454 }
455
456 #[test]
457 fn test_end_ownership_updates_end_date() {
458 let unit_id = Uuid::new_v4();
459 let owner_id = Uuid::new_v4();
460
461 let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
462
463 assert!(unit_owner.end_date.is_none());
464
465 let end_date = Utc::now() + chrono::Duration::days(30);
466 unit_owner.end_ownership(end_date).unwrap();
467
468 assert!(unit_owner.end_date.is_some());
469 assert_eq!(unit_owner.end_date.unwrap(), end_date);
470 }
471
472 #[test]
473 fn test_cannot_end_ownership_twice() {
474 let unit_id = Uuid::new_v4();
475 let owner_id = Uuid::new_v4();
476
477 let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
478
479 let first_end = Utc::now() + chrono::Duration::days(1);
480 unit_owner.end_ownership(first_end).unwrap();
481
482 let second_end = Utc::now() + chrono::Duration::days(2);
484 let result = unit_owner.end_ownership(second_end);
485 assert!(result.is_ok());
486 assert_eq!(unit_owner.end_date.unwrap(), second_end);
487 }
488
489 #[test]
490 fn test_timestamps_are_set() {
491 let unit_id = Uuid::new_v4();
492 let owner_id = Uuid::new_v4();
493
494 let before = Utc::now();
495 let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
496 let after = Utc::now();
497
498 assert!(unit_owner.created_at >= before);
500 assert!(unit_owner.created_at <= after);
501
502 let diff = (unit_owner.created_at - unit_owner.updated_at)
504 .num_milliseconds()
505 .abs();
506 assert!(diff < 1);
507 }
508
509 #[test]
510 fn test_updated_at_changes_on_modification() {
511 let unit_id = Uuid::new_v4();
512 let owner_id = Uuid::new_v4();
513
514 let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
515 let original_updated_at = unit_owner.updated_at;
516
517 std::thread::sleep(std::time::Duration::from_millis(10));
519
520 unit_owner.update_percentage(dec!(0.6)).unwrap();
521 assert!(unit_owner.updated_at > original_updated_at);
522
523 let previous_updated = unit_owner.updated_at;
524 std::thread::sleep(std::time::Duration::from_millis(10));
525
526 unit_owner.set_primary_contact(true);
527 assert!(unit_owner.updated_at > previous_updated);
528 }
529
530 #[test]
531 fn test_100_percent_ownership_is_valid() {
532 let unit_id = Uuid::new_v4();
533 let owner_id = Uuid::new_v4();
534
535 let unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
536 assert_eq!(unit_owner.ownership_percentage, Decimal::ONE);
537 }
538
539 #[test]
540 fn test_multiple_owners_scenario_percentages() {
541 let unit_id = Uuid::new_v4();
542 let owner1_id = Uuid::new_v4();
543 let owner2_id = Uuid::new_v4();
544 let owner3_id = Uuid::new_v4();
545
546 let owner1 = UnitOwner::new(unit_id, owner1_id, dec!(0.5), true).unwrap();
548 let owner2 = UnitOwner::new(unit_id, owner2_id, dec!(0.3), false).unwrap();
549 let owner3 = UnitOwner::new(unit_id, owner3_id, dec!(0.2), false).unwrap();
550
551 assert_eq!(owner1.ownership_percentage, dec!(0.5));
552 assert_eq!(owner2.ownership_percentage, dec!(0.3));
553 assert_eq!(owner3.ownership_percentage, dec!(0.2));
554
555 let total =
557 owner1.ownership_percentage + owner2.ownership_percentage + owner3.ownership_percentage;
558 assert_eq!(total, Decimal::ONE);
559 }
560
561 use std::str::FromStr;
567
568 fn holder(t: OwnershipType, rep: bool) -> LotHolder {
569 LotHolder::new(t, rep)
570 }
571
572 #[test]
574 fn happy_voting_active_mono_full_owner() {
575 let holders = [holder(OwnershipType::FullOwner, false)];
576 assert_eq!(voting_right_status(&holders), VotingRightStatus::Active);
577 assert!(assert_voting_right_active(Uuid::new_v4(), &holders).is_ok());
578 }
579
580 #[test]
582 fn happy_voting_active_with_designated_representative() {
583 let holders = [
585 holder(OwnershipType::Indivisaire, true),
586 holder(OwnershipType::Indivisaire, false),
587 ];
588 assert_eq!(voting_right_status(&holders), VotingRightStatus::Active);
589 assert!(assert_voting_right_active(Uuid::new_v4(), &holders).is_ok());
590 }
591
592 #[test]
595 fn happy_voting_active_legacy_no_holders() {
596 assert_eq!(voting_right_status(&[]), VotingRightStatus::Active);
597 assert!(assert_voting_right_active(Uuid::new_v4(), &[]).is_ok());
598 }
599
600 #[test]
603 fn edge_voting_active_usufruct_with_representative() {
604 let holders = [
605 holder(OwnershipType::Usufruct, true),
606 holder(OwnershipType::BareOwner, false),
607 ];
608 assert_eq!(voting_right_status(&holders), VotingRightStatus::Active);
609 }
610
611 #[test]
614 fn edge_voting_suspended_single_dismembered_holder() {
615 for t in [OwnershipType::Emphyteote, OwnershipType::Superficiaire] {
616 let holders = [holder(t, false)];
617 assert_eq!(
618 voting_right_status(&holders),
619 VotingRightStatus::Suspended,
620 "type {t} seul sans représentant doit suspendre le vote"
621 );
622 }
623 }
624
625 #[test]
627 fn edge_ownership_type_display_fromstr_roundtrip() {
628 for t in [
629 OwnershipType::FullOwner,
630 OwnershipType::Usufruct,
631 OwnershipType::BareOwner,
632 OwnershipType::Indivisaire,
633 OwnershipType::Emphyteote,
634 OwnershipType::Superficiaire,
635 ] {
636 let s = t.to_string();
637 assert_eq!(OwnershipType::from_str(&s).unwrap(), t);
638 }
639 }
640
641 #[test]
644 fn security_voting_suspended_indivision_without_representative() {
645 let unit_id = Uuid::new_v4();
646 let holders = [
647 holder(OwnershipType::Indivisaire, false),
648 holder(OwnershipType::Indivisaire, false),
649 ];
650 assert_eq!(voting_right_status(&holders), VotingRightStatus::Suspended);
651 let err = assert_voting_right_active(unit_id, &holders).unwrap_err();
652 assert_eq!(err.unit_id, unit_id);
653 }
654
655 #[test]
658 fn security_voting_suspended_dismembered_without_representative() {
659 let holders = [
660 holder(OwnershipType::Usufruct, false),
661 holder(OwnershipType::BareOwner, false),
662 ];
663 assert_eq!(voting_right_status(&holders), VotingRightStatus::Suspended);
664 assert!(assert_voting_right_active(Uuid::new_v4(), &holders).is_err());
665 }
666
667 #[test]
670 fn negative_multiple_voting_representatives_rejected() {
671 let unit_id = Uuid::new_v4();
672 let holders = [
673 holder(OwnershipType::Indivisaire, true),
674 holder(OwnershipType::Indivisaire, true),
675 ];
676 let err = assert_single_voting_representative(unit_id, &holders).unwrap_err();
677 match err {
678 VotingRightError::MultipleRepresentatives { unit_id: u, count } => {
679 assert_eq!(u, unit_id);
680 assert_eq!(count, 2);
681 }
682 other => panic!("attendu MultipleRepresentatives, obtenu {other:?}"),
683 }
684 assert!(assert_single_voting_representative(
686 unit_id,
687 &[holder(OwnershipType::Indivisaire, true)]
688 )
689 .is_ok());
690 }
691
692 #[test]
695 fn negative_unknown_ownership_type_rejected() {
696 let err = OwnershipType::from_str("locataire").unwrap_err();
697 assert_eq!(
698 err,
699 VotingRightError::UnknownOwnershipType("locataire".to_string())
700 );
701 }
702}