Skip to main content

koprogo_api/domain/entities/
unit_owner.rs

1use chrono::{DateTime, Utc};
2use rust_decimal::Decimal;
3use uuid::Uuid;
4
5/// UnitOwner represents the ownership relationship between a Unit and an Owner
6/// This entity supports:
7/// - Multiple owners per unit (co-ownership, indivision)
8/// - Multiple units per owner (owner in multiple buildings)
9/// - Ownership percentage tracking
10/// - Historical ownership tracking (start_date, end_date)
11///
12/// MONETARY-ADJACENT: ownership_percentage uses rust_decimal::Decimal (cf. ADR-0007).
13/// Quote-parts drive charge distribution; rounding errors propagate to invoices.
14#[derive(Debug, Clone)]
15pub struct UnitOwner {
16    pub id: Uuid,
17    pub unit_id: Uuid,
18    pub owner_id: Uuid,
19
20    /// Ownership percentage (0.0 to 1.0). Decimal exact (cf. ADR-0007).
21    /// Example: dec!(0.5) = 50%, dec!(1.0) = 100%
22    pub ownership_percentage: Decimal,
23
24    /// Date when ownership started
25    pub start_date: DateTime<Utc>,
26
27    /// Date when ownership ended (None = current owner)
28    pub end_date: Option<DateTime<Utc>>,
29
30    /// Is this owner the primary contact for this unit?
31    pub is_primary_contact: bool,
32
33    pub created_at: DateTime<Utc>,
34    pub updated_at: DateTime<Utc>,
35}
36
37impl UnitOwner {
38    /// Create a new UnitOwner relationship
39    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        // Validate ownership percentage
46        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    /// Create a new UnitOwner with a specific start date
64    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    /// Check if this ownership is currently active
89    pub fn is_active(&self) -> bool {
90        self.end_date.is_none()
91    }
92
93    /// End this ownership relationship
94    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    /// Update ownership percentage
105    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    /// Set as primary contact
116    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#[cfg(test)]
123mod tests {
124    use super::*;
125    use rust_decimal_macros::dec;
126
127    #[test]
128    fn test_create_unit_owner() {
129        let unit_id = Uuid::new_v4();
130        let owner_id = Uuid::new_v4();
131
132        let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), true).unwrap();
133
134        assert_eq!(unit_owner.unit_id, unit_id);
135        assert_eq!(unit_owner.owner_id, owner_id);
136        assert_eq!(unit_owner.ownership_percentage, dec!(0.5));
137        assert!(unit_owner.is_primary_contact);
138        assert!(unit_owner.is_active());
139    }
140
141    #[test]
142    fn test_invalid_ownership_percentage() {
143        let unit_id = Uuid::new_v4();
144        let owner_id = Uuid::new_v4();
145
146        // Test percentage > 1.0
147        let result = UnitOwner::new(unit_id, owner_id, dec!(1.5), false);
148        assert!(result.is_err());
149
150        // Test percentage <= 0
151        let result = UnitOwner::new(unit_id, owner_id, Decimal::ZERO, false);
152        assert!(result.is_err());
153
154        let result = UnitOwner::new(unit_id, owner_id, dec!(-0.5), false);
155        assert!(result.is_err());
156    }
157
158    #[test]
159    fn test_end_ownership() {
160        let unit_id = Uuid::new_v4();
161        let owner_id = Uuid::new_v4();
162
163        let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
164
165        assert!(unit_owner.is_active());
166
167        let end_date = Utc::now() + chrono::Duration::days(1);
168        unit_owner.end_ownership(end_date).unwrap();
169
170        assert!(!unit_owner.is_active());
171        assert_eq!(unit_owner.end_date, Some(end_date));
172    }
173
174    #[test]
175    fn test_invalid_end_date() {
176        let unit_id = Uuid::new_v4();
177        let owner_id = Uuid::new_v4();
178
179        let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
180
181        // End date before start date should fail
182        let invalid_end_date = unit_owner.start_date - chrono::Duration::days(1);
183        let result = unit_owner.end_ownership(invalid_end_date);
184
185        assert!(result.is_err());
186    }
187
188    #[test]
189    fn test_update_percentage() {
190        let unit_id = Uuid::new_v4();
191        let owner_id = Uuid::new_v4();
192
193        let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), true).unwrap();
194
195        unit_owner.update_percentage(dec!(0.75)).unwrap();
196        assert_eq!(unit_owner.ownership_percentage, dec!(0.75));
197
198        // Invalid percentage
199        let result = unit_owner.update_percentage(dec!(1.5));
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn test_update_percentage_boundary_values() {
205        let unit_id = Uuid::new_v4();
206        let owner_id = Uuid::new_v4();
207
208        let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
209
210        // Test boundary: exactly 1.0 (100%) is valid
211        assert!(unit_owner.update_percentage(Decimal::ONE).is_ok());
212        assert_eq!(unit_owner.ownership_percentage, Decimal::ONE);
213
214        // Test boundary: 0.0 is invalid
215        assert!(unit_owner.update_percentage(Decimal::ZERO).is_err());
216
217        // Test boundary: 0.0001 (0.01%) is valid
218        assert!(unit_owner.update_percentage(dec!(0.0001)).is_ok());
219        assert_eq!(unit_owner.ownership_percentage, dec!(0.0001));
220
221        // Test boundary: 1.0001 is invalid
222        assert!(unit_owner.update_percentage(dec!(1.0001)).is_err());
223
224        // Test negative values
225        assert!(unit_owner.update_percentage(dec!(-0.5)).is_err());
226    }
227
228    #[test]
229    fn test_set_primary_contact() {
230        let unit_id = Uuid::new_v4();
231        let owner_id = Uuid::new_v4();
232
233        let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
234
235        assert!(!unit_owner.is_primary_contact);
236
237        unit_owner.set_primary_contact(true);
238        assert!(unit_owner.is_primary_contact);
239
240        unit_owner.set_primary_contact(false);
241        assert!(!unit_owner.is_primary_contact);
242    }
243
244    #[test]
245    fn test_ownership_percentage_precision() {
246        let unit_id = Uuid::new_v4();
247        let owner_id = Uuid::new_v4();
248
249        // Test with 4 decimal places (common for co-ownership)
250        let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.3333), false).unwrap();
251        assert_eq!(unit_owner.ownership_percentage, dec!(0.3333));
252
253        // Test with very small percentage
254        let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.0001), false).unwrap();
255        assert_eq!(unit_owner.ownership_percentage, dec!(0.0001));
256    }
257
258    #[test]
259    fn test_end_ownership_updates_end_date() {
260        let unit_id = Uuid::new_v4();
261        let owner_id = Uuid::new_v4();
262
263        let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
264
265        assert!(unit_owner.end_date.is_none());
266
267        let end_date = Utc::now() + chrono::Duration::days(30);
268        unit_owner.end_ownership(end_date).unwrap();
269
270        assert!(unit_owner.end_date.is_some());
271        assert_eq!(unit_owner.end_date.unwrap(), end_date);
272    }
273
274    #[test]
275    fn test_cannot_end_ownership_twice() {
276        let unit_id = Uuid::new_v4();
277        let owner_id = Uuid::new_v4();
278
279        let mut unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
280
281        let first_end = Utc::now() + chrono::Duration::days(1);
282        unit_owner.end_ownership(first_end).unwrap();
283
284        // Should still work, just updates the date
285        let second_end = Utc::now() + chrono::Duration::days(2);
286        let result = unit_owner.end_ownership(second_end);
287        assert!(result.is_ok());
288        assert_eq!(unit_owner.end_date.unwrap(), second_end);
289    }
290
291    #[test]
292    fn test_timestamps_are_set() {
293        let unit_id = Uuid::new_v4();
294        let owner_id = Uuid::new_v4();
295
296        let before = Utc::now();
297        let unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
298        let after = Utc::now();
299
300        // created_at should be between before and after
301        assert!(unit_owner.created_at >= before);
302        assert!(unit_owner.created_at <= after);
303
304        // updated_at should initially equal created_at (within millisecond precision)
305        let diff = (unit_owner.created_at - unit_owner.updated_at)
306            .num_milliseconds()
307            .abs();
308        assert!(diff < 1);
309    }
310
311    #[test]
312    fn test_updated_at_changes_on_modification() {
313        let unit_id = Uuid::new_v4();
314        let owner_id = Uuid::new_v4();
315
316        let mut unit_owner = UnitOwner::new(unit_id, owner_id, dec!(0.5), false).unwrap();
317        let original_updated_at = unit_owner.updated_at;
318
319        // Wait a tiny bit to ensure timestamp changes
320        std::thread::sleep(std::time::Duration::from_millis(10));
321
322        unit_owner.update_percentage(dec!(0.6)).unwrap();
323        assert!(unit_owner.updated_at > original_updated_at);
324
325        let previous_updated = unit_owner.updated_at;
326        std::thread::sleep(std::time::Duration::from_millis(10));
327
328        unit_owner.set_primary_contact(true);
329        assert!(unit_owner.updated_at > previous_updated);
330    }
331
332    #[test]
333    fn test_100_percent_ownership_is_valid() {
334        let unit_id = Uuid::new_v4();
335        let owner_id = Uuid::new_v4();
336
337        let unit_owner = UnitOwner::new(unit_id, owner_id, Decimal::ONE, true).unwrap();
338        assert_eq!(unit_owner.ownership_percentage, Decimal::ONE);
339    }
340
341    #[test]
342    fn test_multiple_owners_scenario_percentages() {
343        let unit_id = Uuid::new_v4();
344        let owner1_id = Uuid::new_v4();
345        let owner2_id = Uuid::new_v4();
346        let owner3_id = Uuid::new_v4();
347
348        // Scenario: 3 co-owners with 50%, 30%, 20%
349        let owner1 = UnitOwner::new(unit_id, owner1_id, dec!(0.5), true).unwrap();
350        let owner2 = UnitOwner::new(unit_id, owner2_id, dec!(0.3), false).unwrap();
351        let owner3 = UnitOwner::new(unit_id, owner3_id, dec!(0.2), false).unwrap();
352
353        assert_eq!(owner1.ownership_percentage, dec!(0.5));
354        assert_eq!(owner2.ownership_percentage, dec!(0.3));
355        assert_eq!(owner3.ownership_percentage, dec!(0.2));
356
357        // Total should be 1.0 EXACTLY (Decimal — pas IEEE 754).
358        let total =
359            owner1.ownership_percentage + owner2.ownership_percentage + owner3.ownership_percentage;
360        assert_eq!(total, Decimal::ONE);
361    }
362}