koprogo_api/domain/entities/
unit_owner.rs

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