Skip to main content

koprogo_api/application/use_cases/
unit_owner_use_cases.rs

1use 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    /// Add an owner to a unit with specified ownership percentage
32    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        // Validate that unit exists
40        self.unit_repository
41            .find_by_id(unit_id)
42            .await?
43            .ok_or("Unit not found")?;
44
45        // Validate that owner exists
46        self.owner_repository
47            .find_by_id(owner_id)
48            .await?
49            .ok_or("Owner not found")?;
50
51        // Check if this owner is already active on this unit
52        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        // Validate total ownership percentage won't exceed 100% (Art. 577-2 §4 CC)
61        let current_total = self
62            .unit_owner_repository
63            .get_total_ownership_percentage(unit_id)
64            .await?;
65
66        // CRITICAL: This validation MUST block if total > 100.0% (Belgian legal requirement)
67        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 this is primary contact, unset any existing primary contact
78        if is_primary_contact {
79            self.unset_all_primary_contacts(unit_id).await?;
80        }
81
82        // Create the unit-owner relationship
83        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    /// Remove an owner from a unit (sets end_date to now)
90    pub async fn remove_owner_from_unit(
91        &self,
92        unit_id: Uuid,
93        owner_id: Uuid,
94    ) -> Result<UnitOwner, String> {
95        // Find the active relationship
96        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        // End the ownership
103        unit_owner.end_ownership(Utc::now())?;
104
105        self.unit_owner_repository.update(&unit_owner).await
106    }
107
108    /// Update the ownership percentage for a unit-owner relationship
109    pub async fn update_ownership_percentage(
110        &self,
111        unit_owner_id: Uuid,
112        new_percentage: Decimal,
113    ) -> Result<UnitOwner, String> {
114        // Find the unit-owner relationship
115        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        // Validate it's still active
122        if !unit_owner.is_active() {
123            return Err("Cannot update percentage of ended ownership".to_string());
124        }
125
126        // Calculate what the new total would be
127        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        // CRITICAL: This validation MUST block if total > 100.0% (Art. 577-2 §4 CC)
135        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        // Update the percentage
146        unit_owner.update_percentage(new_percentage)?;
147
148        self.unit_owner_repository.update(&unit_owner).await
149    }
150
151    /// Transfer ownership from one owner to another
152    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        // Validate that both owners exist
159        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        // Get the active relationship from the source owner
170        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        // Check if target owner already has an active relationship
177        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        // End the source ownership
186        let transfer_date = Utc::now();
187        from_relationship.end_ownership(transfer_date)?;
188
189        // Create new ownership for target owner with same percentage
190        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        // Update both relationships
198        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    /// Get all current owners of a unit
208    pub async fn get_unit_owners(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
209        // Validate unit exists
210        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    /// Get all current units owned by an owner
221    pub async fn get_owner_units(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
222        // Validate owner exists
223        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    /// Get ownership history for a unit (including past owners)
234    pub async fn get_unit_ownership_history(
235        &self,
236        unit_id: Uuid,
237    ) -> Result<Vec<UnitOwner>, String> {
238        // Validate unit exists
239        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    /// Get ownership history for an owner (including past units)
250    pub async fn get_owner_ownership_history(
251        &self,
252        owner_id: Uuid,
253    ) -> Result<Vec<UnitOwner>, String> {
254        // Validate owner exists
255        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    /// Set a unit-owner relationship as primary contact
266    pub async fn set_primary_contact(&self, unit_owner_id: Uuid) -> Result<UnitOwner, String> {
267        // Find the unit-owner relationship
268        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        // Validate it's still active
275        if !unit_owner.is_active() {
276            return Err("Cannot set primary contact for ended ownership".to_string());
277        }
278
279        // Unset all other primary contacts for this unit
280        self.unset_all_primary_contacts(unit_owner.unit_id).await?;
281
282        // Set this one as primary
283        unit_owner.set_primary_contact(true);
284
285        self.unit_owner_repository.update(&unit_owner).await
286    }
287
288    /// Get a specific unit-owner relationship by ID
289    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    /// Check if a unit has any active owners
294    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    /// Get the total ownership percentage for a unit
299    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    /// Désigne `owner_id` comme représentant de vote unique du lot `unit_id`
306    /// (Art. 3.87 §1 CC, #848). Un lot à plusieurs titulaires actifs (couple,
307    /// succession — le cas le plus ordinaire) voit son vote SUSPENDU tant
308    /// qu'aucun d'eux n'est désigné ; cette méthode livre la désignation qui
309    /// lève cette suspension (`voting_right_status` redevient `Active`).
310    ///
311    /// Idempotent : redésigner le représentant déjà en place est un no-op, pas
312    /// une seconde désignation.
313    ///
314    /// Refuse (`AppError::Conflict`, via `VotingRightError::MultipleRepresentatives`)
315    /// si un AUTRE titulaire du lot est déjà désigné : l'Art. 3.87 §1 prévoit
316    /// un représentant UNIQUE. C'est le contrôle dormant nommé par #848 —
317    /// `assert_single_voting_representative` — appelé ici pour la première
318    /// fois en production, sur l'état PROSPECTIF (les titulaires actuels plus
319    /// la désignation qui vient), avant toute écriture.
320    pub async fn designate_voting_representative(
321        &self,
322        unit_id: Uuid,
323        owner_id: Uuid,
324    ) -> Result<UnitOwner, AppError> {
325        // La désignation ne peut porter que sur une titularité ACTIVE de CE
326        // lot : ni un rattachement clos, ni un autre lot que celui visé par la
327        // route (cf. handler — `verify_unit_org_access` filtre déjà le
328        // cloisonnement organisation, ceci filtre la cohérence des données).
329        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            // Déjà désigné : rien à écrire, rien à contrôler à nouveau.
347            return Ok(target);
348        }
349
350        // État prospectif = titulaires actuels (aucun n'est le représentant
351        // visé, on vient de le vérifier) + la désignation qui vient. Le type
352        // de titularité du titulaire ajouté n'entre pas dans le calcul
353        // d'`assert_single_voting_representative` (il ne compte que
354        // `is_voting_representative`) : `FullOwner` par défaut n'introduit
355        // aucun biais.
356        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    // Helper method to unset all primary contacts for a unit
373    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// Déclaré en BAS de fichier, pas en haut : la garde `garde_controles_dormants`
391// coupe chaque fichier à la première occurrence textuelle de l'attribut
392// cfg(test), et ne scanne que ce qui précède pour trouver les appels de
393// production. Cet attribut en tête de fichier aurait fait disparaître TOUT
394// l'`impl UnitOwnerUseCases` ci-dessus — donc l'appel de
395// `assert_single_voting_representative` (#848) — de son scan, malgré un appel
396// bien réel. Repéré en écrivant cette story ; aucun autre fichier de
397// `use_cases/` ne déclare son module de test de cette façon en tête.
398#[cfg(test)]
399#[path = "unit_owner_use_cases_test.rs"]
400mod unit_owner_use_cases_test;