koprogo_api/application/use_cases/
unit_owner_use_cases.rs1use crate::application::error::AppError;
2use crate::application::ports::{OwnerRepository, UnitOwnerRepository, UnitRepository};
3use crate::domain::entities::{
4 assert_single_voting_representative, LotHolder, OwnershipType, UnitOwner,
5};
6use chrono::Utc;
7use rust_decimal::Decimal;
8use rust_decimal_macros::dec;
9use std::sync::Arc;
10use uuid::Uuid;
11
12pub struct UnitOwnerUseCases {
13 unit_owner_repository: Arc<dyn UnitOwnerRepository>,
14 unit_repository: Arc<dyn UnitRepository>,
15 owner_repository: Arc<dyn OwnerRepository>,
16}
17
18impl UnitOwnerUseCases {
19 pub fn new(
20 unit_owner_repository: Arc<dyn UnitOwnerRepository>,
21 unit_repository: Arc<dyn UnitRepository>,
22 owner_repository: Arc<dyn OwnerRepository>,
23 ) -> Self {
24 Self {
25 unit_owner_repository,
26 unit_repository,
27 owner_repository,
28 }
29 }
30
31 pub async fn add_owner_to_unit(
33 &self,
34 unit_id: Uuid,
35 owner_id: Uuid,
36 ownership_percentage: Decimal,
37 is_primary_contact: bool,
38 ) -> Result<UnitOwner, String> {
39 self.unit_repository
41 .find_by_id(unit_id)
42 .await?
43 .ok_or("Unit not found")?;
44
45 self.owner_repository
47 .find_by_id(owner_id)
48 .await?
49 .ok_or("Owner not found")?;
50
51 if let Some(_existing) = self
53 .unit_owner_repository
54 .find_active_by_unit_and_owner(unit_id, owner_id)
55 .await?
56 {
57 return Err("Owner is already active on this unit".to_string());
58 }
59
60 let current_total = self
62 .unit_owner_repository
63 .get_total_ownership_percentage(unit_id)
64 .await?;
65
66 if current_total + ownership_percentage > Decimal::ONE {
68 return Err(format!(
69 "Total ownership would exceed 100% (Art. 577-2 §4 CC). \
70 Current: {}%, adding: {}%, total would be: {}%",
71 current_total * dec!(100),
72 ownership_percentage * dec!(100),
73 (current_total + ownership_percentage) * dec!(100)
74 ));
75 }
76
77 if is_primary_contact {
79 self.unset_all_primary_contacts(unit_id).await?;
80 }
81
82 let unit_owner =
84 UnitOwner::new(unit_id, owner_id, ownership_percentage, is_primary_contact)?;
85
86 self.unit_owner_repository.create(&unit_owner).await
87 }
88
89 pub async fn remove_owner_from_unit(
91 &self,
92 unit_id: Uuid,
93 owner_id: Uuid,
94 ) -> Result<UnitOwner, String> {
95 let mut unit_owner = self
97 .unit_owner_repository
98 .find_active_by_unit_and_owner(unit_id, owner_id)
99 .await?
100 .ok_or("Active unit-owner relationship not found")?;
101
102 unit_owner.end_ownership(Utc::now())?;
104
105 self.unit_owner_repository.update(&unit_owner).await
106 }
107
108 pub async fn update_ownership_percentage(
110 &self,
111 unit_owner_id: Uuid,
112 new_percentage: Decimal,
113 ) -> Result<UnitOwner, String> {
114 let mut unit_owner = self
116 .unit_owner_repository
117 .find_by_id(unit_owner_id)
118 .await?
119 .ok_or("Unit-owner relationship not found")?;
120
121 if !unit_owner.is_active() {
123 return Err("Cannot update percentage of ended ownership".to_string());
124 }
125
126 let current_total = self
128 .unit_owner_repository
129 .get_total_ownership_percentage(unit_owner.unit_id)
130 .await?;
131 let old_percentage = unit_owner.ownership_percentage;
132 let new_total = current_total - old_percentage + new_percentage;
133
134 if new_total > Decimal::ONE {
136 return Err(format!(
137 "Total ownership would exceed 100% (Art. 577-2 §4 CC). \
138 Current without this owner: {}%, new percentage: {}%, total would be: {}%",
139 (current_total - old_percentage) * dec!(100),
140 new_percentage * dec!(100),
141 new_total * dec!(100)
142 ));
143 }
144
145 unit_owner.update_percentage(new_percentage)?;
147
148 self.unit_owner_repository.update(&unit_owner).await
149 }
150
151 pub async fn transfer_ownership(
153 &self,
154 from_owner_id: Uuid,
155 to_owner_id: Uuid,
156 unit_id: Uuid,
157 ) -> Result<(UnitOwner, UnitOwner), String> {
158 self.owner_repository
160 .find_by_id(from_owner_id)
161 .await?
162 .ok_or("Source owner not found")?;
163
164 self.owner_repository
165 .find_by_id(to_owner_id)
166 .await?
167 .ok_or("Target owner not found")?;
168
169 let mut from_relationship = self
171 .unit_owner_repository
172 .find_active_by_unit_and_owner(unit_id, from_owner_id)
173 .await?
174 .ok_or("Source owner does not own this unit")?;
175
176 if let Some(_existing) = self
178 .unit_owner_repository
179 .find_active_by_unit_and_owner(unit_id, to_owner_id)
180 .await?
181 {
182 return Err("Target owner already owns this unit".to_string());
183 }
184
185 let transfer_date = Utc::now();
187 from_relationship.end_ownership(transfer_date)?;
188
189 let to_relationship = UnitOwner::new(
191 unit_id,
192 to_owner_id,
193 from_relationship.ownership_percentage,
194 from_relationship.is_primary_contact,
195 )?;
196
197 let ended_relationship = self
199 .unit_owner_repository
200 .update(&from_relationship)
201 .await?;
202 let new_relationship = self.unit_owner_repository.create(&to_relationship).await?;
203
204 Ok((ended_relationship, new_relationship))
205 }
206
207 pub async fn get_unit_owners(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
209 self.unit_repository
211 .find_by_id(unit_id)
212 .await?
213 .ok_or("Unit not found")?;
214
215 self.unit_owner_repository
216 .find_current_owners_by_unit(unit_id)
217 .await
218 }
219
220 pub async fn get_owner_units(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
222 self.owner_repository
224 .find_by_id(owner_id)
225 .await?
226 .ok_or("Owner not found")?;
227
228 self.unit_owner_repository
229 .find_current_units_by_owner(owner_id)
230 .await
231 }
232
233 pub async fn get_unit_ownership_history(
235 &self,
236 unit_id: Uuid,
237 ) -> Result<Vec<UnitOwner>, String> {
238 self.unit_repository
240 .find_by_id(unit_id)
241 .await?
242 .ok_or("Unit not found")?;
243
244 self.unit_owner_repository
245 .find_all_owners_by_unit(unit_id)
246 .await
247 }
248
249 pub async fn get_owner_ownership_history(
251 &self,
252 owner_id: Uuid,
253 ) -> Result<Vec<UnitOwner>, String> {
254 self.owner_repository
256 .find_by_id(owner_id)
257 .await?
258 .ok_or("Owner not found")?;
259
260 self.unit_owner_repository
261 .find_all_units_by_owner(owner_id)
262 .await
263 }
264
265 pub async fn set_primary_contact(&self, unit_owner_id: Uuid) -> Result<UnitOwner, String> {
267 let mut unit_owner = self
269 .unit_owner_repository
270 .find_by_id(unit_owner_id)
271 .await?
272 .ok_or("Unit-owner relationship not found")?;
273
274 if !unit_owner.is_active() {
276 return Err("Cannot set primary contact for ended ownership".to_string());
277 }
278
279 self.unset_all_primary_contacts(unit_owner.unit_id).await?;
281
282 unit_owner.set_primary_contact(true);
284
285 self.unit_owner_repository.update(&unit_owner).await
286 }
287
288 pub async fn get_unit_owner(&self, id: Uuid) -> Result<Option<UnitOwner>, String> {
290 self.unit_owner_repository.find_by_id(id).await
291 }
292
293 pub async fn has_active_owners(&self, unit_id: Uuid) -> Result<bool, String> {
295 self.unit_owner_repository.has_active_owners(unit_id).await
296 }
297
298 pub async fn get_total_ownership_percentage(&self, unit_id: Uuid) -> Result<Decimal, String> {
300 self.unit_owner_repository
301 .get_total_ownership_percentage(unit_id)
302 .await
303 }
304
305 pub async fn designate_voting_representative(
321 &self,
322 unit_id: Uuid,
323 owner_id: Uuid,
324 ) -> Result<UnitOwner, AppError> {
325 let target = self
330 .unit_owner_repository
331 .find_active_by_unit_and_owner(unit_id, owner_id)
332 .await
333 .map_err(AppError::from)?
334 .ok_or_else(|| {
335 AppError::NotFound(format!(
336 "Aucune titularité active de l'owner {owner_id} sur le lot {unit_id}"
337 ))
338 })?;
339
340 if self
341 .unit_owner_repository
342 .is_voting_representative(target.id)
343 .await
344 .map_err(AppError::from)?
345 {
346 return Ok(target);
348 }
349
350 let mut prospective: Vec<LotHolder> = self
357 .unit_owner_repository
358 .find_voting_holders_by_unit(unit_id)
359 .await
360 .map_err(AppError::from)?;
361 prospective.push(LotHolder::new(OwnershipType::default(), true));
362 assert_single_voting_representative(unit_id, &prospective).map_err(AppError::from)?;
363
364 self.unit_owner_repository
365 .set_voting_representative(target.id)
366 .await
367 .map_err(AppError::from)?;
368
369 Ok(target)
370 }
371
372 async fn unset_all_primary_contacts(&self, unit_id: Uuid) -> Result<(), String> {
374 let current_owners = self
375 .unit_owner_repository
376 .find_current_owners_by_unit(unit_id)
377 .await?;
378
379 for mut owner in current_owners {
380 if owner.is_primary_contact {
381 owner.set_primary_contact(false);
382 self.unit_owner_repository.update(&owner).await?;
383 }
384 }
385
386 Ok(())
387 }
388}
389
390#[cfg(test)]
399#[path = "unit_owner_use_cases_test.rs"]
400mod unit_owner_use_cases_test;