Skip to main content

koprogo_api/infrastructure/database/repositories/
unit_owner_repository_impl.rs

1//! PostgreSQL impl du UnitOwnerRepository.
2//!
3//! ADR-0007/0008 : `ownership_percentage` est `Decimal` end-to-end
4//! (domain + SQL NUMERIC(6,5) depuis migration `20260501000000`).
5
6use crate::application::ports::UnitOwnerRepository;
7use crate::domain::entities::UnitOwner;
8use async_trait::async_trait;
9use rust_decimal::Decimal;
10use sqlx::PgPool;
11use uuid::Uuid;
12
13pub struct PostgresUnitOwnerRepository {
14    pool: PgPool,
15}
16
17impl PostgresUnitOwnerRepository {
18    pub fn new(pool: PgPool) -> Self {
19        Self { pool }
20    }
21}
22
23#[async_trait]
24impl UnitOwnerRepository for PostgresUnitOwnerRepository {
25    async fn create(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
26        let result = sqlx::query!(
27            r#"
28            INSERT INTO unit_owners (
29                id, unit_id, owner_id, ownership_percentage,
30                start_date, end_date, is_primary_contact,
31                created_at, updated_at
32            )
33            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
34            RETURNING *
35            "#,
36            unit_owner.id,
37            unit_owner.unit_id,
38            unit_owner.owner_id,
39            unit_owner.ownership_percentage,
40            unit_owner.start_date,
41            unit_owner.end_date,
42            unit_owner.is_primary_contact,
43            unit_owner.created_at,
44            unit_owner.updated_at
45        )
46        .fetch_one(&self.pool)
47        .await
48        .map_err(|e| format!("Failed to create unit_owner: {}", e))?;
49
50        Ok(UnitOwner {
51            id: result.id,
52            unit_id: result.unit_id,
53            owner_id: result.owner_id,
54            ownership_percentage: result.ownership_percentage,
55            start_date: result.start_date,
56            end_date: result.end_date,
57            is_primary_contact: result.is_primary_contact,
58            created_at: result.created_at,
59            updated_at: result.updated_at,
60        })
61    }
62
63    async fn find_by_id(&self, id: Uuid) -> Result<Option<UnitOwner>, String> {
64        let result = sqlx::query!(
65            r#"
66            SELECT * FROM unit_owners WHERE id = $1
67            "#,
68            id
69        )
70        .fetch_optional(&self.pool)
71        .await
72        .map_err(|e| format!("Failed to find unit_owner: {}", e))?;
73
74        Ok(result.map(|row| UnitOwner {
75            id: row.id,
76            unit_id: row.unit_id,
77            owner_id: row.owner_id,
78            ownership_percentage: row.ownership_percentage,
79            start_date: row.start_date,
80            end_date: row.end_date,
81            is_primary_contact: row.is_primary_contact,
82            created_at: row.created_at,
83            updated_at: row.updated_at,
84        }))
85    }
86
87    async fn find_current_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
88        let results = sqlx::query!(
89            r#"
90            SELECT * FROM unit_owners
91            WHERE unit_id = $1 AND end_date IS NULL
92            ORDER BY is_primary_contact DESC, created_at ASC
93            "#,
94            unit_id
95        )
96        .fetch_all(&self.pool)
97        .await
98        .map_err(|e| format!("Failed to find owners by unit: {}", e))?;
99
100        Ok(results
101            .into_iter()
102            .map(|row| UnitOwner {
103                id: row.id,
104                unit_id: row.unit_id,
105                owner_id: row.owner_id,
106                ownership_percentage: row.ownership_percentage,
107                start_date: row.start_date,
108                end_date: row.end_date,
109                is_primary_contact: row.is_primary_contact,
110                created_at: row.created_at,
111                updated_at: row.updated_at,
112            })
113            .collect())
114    }
115
116    async fn find_current_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
117        let results = sqlx::query!(
118            r#"
119            SELECT * FROM unit_owners
120            WHERE owner_id = $1 AND end_date IS NULL
121            ORDER BY created_at ASC
122            "#,
123            owner_id
124        )
125        .fetch_all(&self.pool)
126        .await
127        .map_err(|e| format!("Failed to find units by owner: {}", e))?;
128
129        Ok(results
130            .into_iter()
131            .map(|row| UnitOwner {
132                id: row.id,
133                unit_id: row.unit_id,
134                owner_id: row.owner_id,
135                ownership_percentage: row.ownership_percentage,
136                start_date: row.start_date,
137                end_date: row.end_date,
138                is_primary_contact: row.is_primary_contact,
139                created_at: row.created_at,
140                updated_at: row.updated_at,
141            })
142            .collect())
143    }
144
145    async fn find_all_owners_by_unit(&self, unit_id: Uuid) -> Result<Vec<UnitOwner>, String> {
146        let results = sqlx::query!(
147            r#"
148            SELECT * FROM unit_owners
149            WHERE unit_id = $1
150            ORDER BY start_date DESC
151            "#,
152            unit_id
153        )
154        .fetch_all(&self.pool)
155        .await
156        .map_err(|e| format!("Failed to find all owners by unit: {}", e))?;
157
158        Ok(results
159            .into_iter()
160            .map(|row| UnitOwner {
161                id: row.id,
162                unit_id: row.unit_id,
163                owner_id: row.owner_id,
164                ownership_percentage: row.ownership_percentage,
165                start_date: row.start_date,
166                end_date: row.end_date,
167                is_primary_contact: row.is_primary_contact,
168                created_at: row.created_at,
169                updated_at: row.updated_at,
170            })
171            .collect())
172    }
173
174    async fn find_all_units_by_owner(&self, owner_id: Uuid) -> Result<Vec<UnitOwner>, String> {
175        let results = sqlx::query!(
176            r#"
177            SELECT * FROM unit_owners
178            WHERE owner_id = $1
179            ORDER BY start_date DESC
180            "#,
181            owner_id
182        )
183        .fetch_all(&self.pool)
184        .await
185        .map_err(|e| format!("Failed to find all units by owner: {}", e))?;
186
187        Ok(results
188            .into_iter()
189            .map(|row| UnitOwner {
190                id: row.id,
191                unit_id: row.unit_id,
192                owner_id: row.owner_id,
193                ownership_percentage: row.ownership_percentage,
194                start_date: row.start_date,
195                end_date: row.end_date,
196                is_primary_contact: row.is_primary_contact,
197                created_at: row.created_at,
198                updated_at: row.updated_at,
199            })
200            .collect())
201    }
202
203    async fn update(&self, unit_owner: &UnitOwner) -> Result<UnitOwner, String> {
204        let result = sqlx::query!(
205            r#"
206            UPDATE unit_owners
207            SET ownership_percentage = $2,
208                end_date = $3,
209                is_primary_contact = $4,
210                updated_at = $5
211            WHERE id = $1
212            RETURNING *
213            "#,
214            unit_owner.id,
215            unit_owner.ownership_percentage,
216            unit_owner.end_date,
217            unit_owner.is_primary_contact,
218            unit_owner.updated_at
219        )
220        .fetch_one(&self.pool)
221        .await
222        .map_err(|e| format!("Failed to update unit_owner: {}", e))?;
223
224        Ok(UnitOwner {
225            id: result.id,
226            unit_id: result.unit_id,
227            owner_id: result.owner_id,
228            ownership_percentage: result.ownership_percentage,
229            start_date: result.start_date,
230            end_date: result.end_date,
231            is_primary_contact: result.is_primary_contact,
232            created_at: result.created_at,
233            updated_at: result.updated_at,
234        })
235    }
236
237    async fn delete(&self, id: Uuid) -> Result<(), String> {
238        sqlx::query!(
239            r#"
240            DELETE FROM unit_owners WHERE id = $1
241            "#,
242            id
243        )
244        .execute(&self.pool)
245        .await
246        .map_err(|e| format!("Failed to delete unit_owner: {}", e))?;
247
248        Ok(())
249    }
250
251    async fn has_active_owners(&self, unit_id: Uuid) -> Result<bool, String> {
252        let result = sqlx::query!(
253            r#"
254            SELECT EXISTS(SELECT 1 FROM unit_owners WHERE unit_id = $1 AND end_date IS NULL) as "exists!"
255            "#,
256            unit_id
257        )
258        .fetch_one(&self.pool)
259        .await
260        .map_err(|e| format!("Failed to check active owners: {}", e))?;
261
262        Ok(result.exists)
263    }
264
265    async fn get_total_ownership_percentage(&self, unit_id: Uuid) -> Result<Decimal, String> {
266        let result = sqlx::query!(
267            r#"
268            SELECT COALESCE(SUM(ownership_percentage), 0) as "total!"
269            FROM unit_owners
270            WHERE unit_id = $1 AND end_date IS NULL
271            "#,
272            unit_id
273        )
274        .fetch_one(&self.pool)
275        .await
276        .map_err(|e| format!("Failed to get total ownership percentage: {}", e))?;
277
278        Ok(result.total)
279    }
280
281    async fn find_active_by_unit_and_owner(
282        &self,
283        unit_id: Uuid,
284        owner_id: Uuid,
285    ) -> Result<Option<UnitOwner>, String> {
286        let result = sqlx::query!(
287            r#"
288            SELECT * FROM unit_owners
289            WHERE unit_id = $1 AND owner_id = $2 AND end_date IS NULL
290            "#,
291            unit_id,
292            owner_id
293        )
294        .fetch_optional(&self.pool)
295        .await
296        .map_err(|e| format!("Failed to find active unit_owner: {}", e))?;
297
298        Ok(result.map(|row| UnitOwner {
299            id: row.id,
300            unit_id: row.unit_id,
301            owner_id: row.owner_id,
302            ownership_percentage: row.ownership_percentage,
303            start_date: row.start_date,
304            end_date: row.end_date,
305            is_primary_contact: row.is_primary_contact,
306            created_at: row.created_at,
307            updated_at: row.updated_at,
308        }))
309    }
310
311    async fn find_active_by_building(
312        &self,
313        building_id: Uuid,
314    ) -> Result<Vec<(Uuid, Uuid, Decimal)>, String> {
315        let results = sqlx::query!(
316            r#"
317            SELECT uo.unit_id, uo.owner_id, uo.ownership_percentage
318            FROM unit_owners uo
319            JOIN units u ON uo.unit_id = u.id
320            WHERE u.building_id = $1 AND uo.end_date IS NULL
321            "#,
322            building_id
323        )
324        .fetch_all(&self.pool)
325        .await
326        .map_err(|e| format!("Failed to find active unit_owners by building: {}", e))?;
327
328        Ok(results
329            .into_iter()
330            .map(|row| (row.unit_id, row.owner_id, row.ownership_percentage))
331            .collect())
332    }
333}