Skip to main content

koprogo_api/infrastructure/database/repositories/
owner_repository_impl.rs

1use crate::application::dto::{OwnerFilters, PageRequest};
2use crate::application::ports::OwnerRepository;
3use crate::domain::entities::Owner;
4use crate::infrastructure::database::pool::DbPool;
5use async_trait::async_trait;
6use sqlx::Row;
7use uuid::Uuid;
8
9pub struct PostgresOwnerRepository {
10    pool: DbPool,
11}
12
13impl PostgresOwnerRepository {
14    pub fn new(pool: DbPool) -> Self {
15        Self { pool }
16    }
17}
18
19#[async_trait]
20impl OwnerRepository for PostgresOwnerRepository {
21    async fn create(&self, owner: &Owner) -> Result<Owner, String> {
22        sqlx::query(
23            r#"
24            INSERT INTO owners (id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at)
25            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
26            "#,
27        )
28        .bind(owner.id)
29        .bind(owner.organization_id)
30        .bind(owner.user_id)
31        .bind(&owner.first_name)
32        .bind(&owner.last_name)
33        .bind(&owner.email)
34        .bind(&owner.phone)
35        .bind(&owner.address)
36        .bind(&owner.city)
37        .bind(&owner.postal_code)
38        .bind(&owner.country)
39        .bind(owner.created_at)
40        .bind(owner.updated_at)
41        .execute(&self.pool)
42        .await
43        .map_err(|e| format!("Database error: {}", e))?;
44
45        Ok(owner.clone())
46    }
47
48    async fn find_by_id(&self, id: Uuid) -> Result<Option<Owner>, String> {
49        let row = sqlx::query(
50            r#"
51            SELECT id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at
52            FROM owners
53            WHERE id = $1
54            "#,
55        )
56        .bind(id)
57        .fetch_optional(&self.pool)
58        .await
59        .map_err(|e| format!("Database error: {}", e))?;
60
61        Ok(row.map(|row| Owner {
62            id: row.get("id"),
63            organization_id: row.get("organization_id"),
64            user_id: row.get("user_id"),
65            first_name: row.get("first_name"),
66            last_name: row.get("last_name"),
67            email: row.get("email"),
68            phone: row.get("phone"),
69            address: row.get("address"),
70            city: row.get("city"),
71            postal_code: row.get("postal_code"),
72            country: row.get("country"),
73            created_at: row.get("created_at"),
74            updated_at: row.get("updated_at"),
75        }))
76    }
77
78    async fn find_by_user_id(&self, user_id: Uuid) -> Result<Option<Owner>, String> {
79        let row = sqlx::query(
80            r#"
81            SELECT id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at
82            FROM owners
83            WHERE user_id = $1
84            "#,
85        )
86        .bind(user_id)
87        .fetch_optional(&self.pool)
88        .await
89        .map_err(|e| format!("Database error: {}", e))?;
90
91        Ok(row.map(|row| Owner {
92            id: row.get("id"),
93            organization_id: row.get("organization_id"),
94            user_id: row.get("user_id"),
95            first_name: row.get("first_name"),
96            last_name: row.get("last_name"),
97            email: row.get("email"),
98            phone: row.get("phone"),
99            address: row.get("address"),
100            city: row.get("city"),
101            postal_code: row.get("postal_code"),
102            country: row.get("country"),
103            created_at: row.get("created_at"),
104            updated_at: row.get("updated_at"),
105        }))
106    }
107
108    async fn find_by_user_id_and_organization(
109        &self,
110        user_id: Uuid,
111        organization_id: Uuid,
112    ) -> Result<Option<Owner>, String> {
113        let row = sqlx::query(
114            r#"
115            SELECT id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at
116            FROM owners
117            WHERE user_id = $1 AND organization_id = $2
118            "#,
119        )
120        .bind(user_id)
121        .bind(organization_id)
122        .fetch_optional(&self.pool)
123        .await
124        .map_err(|e| format!("Database error: {}", e))?;
125
126        Ok(row.map(|row| Owner {
127            id: row.get("id"),
128            organization_id: row.get("organization_id"),
129            user_id: row.get("user_id"),
130            first_name: row.get("first_name"),
131            last_name: row.get("last_name"),
132            email: row.get("email"),
133            phone: row.get("phone"),
134            address: row.get("address"),
135            city: row.get("city"),
136            postal_code: row.get("postal_code"),
137            country: row.get("country"),
138            created_at: row.get("created_at"),
139            updated_at: row.get("updated_at"),
140        }))
141    }
142
143    async fn find_by_email(&self, email: &str) -> Result<Option<Owner>, String> {
144        let row = sqlx::query(
145            r#"
146            SELECT id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at
147            FROM owners
148            WHERE email = $1
149            "#,
150        )
151        .bind(email)
152        .fetch_optional(&self.pool)
153        .await
154        .map_err(|e| format!("Database error: {}", e))?;
155
156        Ok(row.map(|row| Owner {
157            id: row.get("id"),
158            organization_id: row.get("organization_id"),
159            user_id: row.get("user_id"),
160            first_name: row.get("first_name"),
161            last_name: row.get("last_name"),
162            email: row.get("email"),
163            phone: row.get("phone"),
164            address: row.get("address"),
165            city: row.get("city"),
166            postal_code: row.get("postal_code"),
167            country: row.get("country"),
168            created_at: row.get("created_at"),
169            updated_at: row.get("updated_at"),
170        }))
171    }
172
173    async fn find_all(&self) -> Result<Vec<Owner>, String> {
174        let rows = sqlx::query(
175            r#"
176            SELECT id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at
177            FROM owners
178            ORDER BY last_name, first_name
179            "#,
180        )
181        .fetch_all(&self.pool)
182        .await
183        .map_err(|e| format!("Database error: {}", e))?;
184
185        Ok(rows
186            .iter()
187            .map(|row| Owner {
188                id: row.get("id"),
189                organization_id: row.get("organization_id"),
190                user_id: row.get("user_id"),
191                first_name: row.get("first_name"),
192                last_name: row.get("last_name"),
193                email: row.get("email"),
194                phone: row.get("phone"),
195                address: row.get("address"),
196                city: row.get("city"),
197                postal_code: row.get("postal_code"),
198                country: row.get("country"),
199                created_at: row.get("created_at"),
200                updated_at: row.get("updated_at"),
201            })
202            .collect())
203    }
204
205    async fn find_all_paginated(
206        &self,
207        page_request: &PageRequest,
208        filters: &OwnerFilters,
209    ) -> Result<(Vec<Owner>, i64), String> {
210        // Validate page request
211        page_request.validate()?;
212
213        // Build WHERE clause dynamically
214        let mut where_clauses = Vec::new();
215        let mut param_count = 0;
216
217        // Portée par DÉTENTION, pas par estampille de saisie.
218        //
219        // Un copropriétaire est, en droit, une personne qui DÉTIENT UN LOT :
220        // l'Art. 3.84 CC répartit le droit de propriété « par lots », et
221        // l'Art. 3.86 § 1er fait naître l'ACP par « la cession ou
222        // l'attribution d'un lot au moins ». La notion de copropriétaire sans
223        // lot n'existe pas. Son rattachement à une copropriété passe donc par
224        // ses détentions, jamais par le cabinet qui a saisi sa fiche.
225        //
226        // PAS de colonne `owners.acp_id` : une même personne peut détenir des
227        // lots dans PLUSIEURS copropriétés — un appartement ici, un garage
228        // ailleurs. Une clé singulière serait fausse dès le premier
229        // investisseur. La jointure dit la vérité, une colonne mentirait.
230        //
231        // La détention est prise SANS filtrer sur `end_date` : un vendeur
232        // reste débiteur de ses arriérés après la mutation (Art. 3.94 § 2),
233        // et le syndic doit pouvoir les poursuivre. Un ancien copropriétaire
234        // sort du présent, pas des livres.
235        //
236        // Le repli sur `organization_id` ne vaut que pour une fiche pas
237        // encore rattachée à un lot : entre sa création et l'enregistrement
238        // de la détention, elle disparaîtrait autrement de toutes les listes.
239        // C'est un brouillon, visible de son seul auteur.
240        if filters.organization_id.is_some() {
241            param_count += 1;
242            where_clauses.push(format!(
243                "(EXISTS (SELECT 1 FROM unit_owners uo \
244                          JOIN units u ON u.id = uo.unit_id \
245                          JOIN buildings b ON b.id = u.building_id \
246                          JOIN acps a ON a.id = b.acp_id \
247                          WHERE uo.owner_id = owners.id AND a.organization_id = ${p}) \
248                  OR (NOT EXISTS (SELECT 1 FROM unit_owners uo WHERE uo.owner_id = owners.id) \
249                      AND owners.organization_id = ${p}))",
250                p = param_count
251            ));
252        }
253
254        if filters.email.is_some() {
255            param_count += 1;
256            where_clauses.push(format!("email ILIKE ${}", param_count));
257        }
258
259        if filters.phone.is_some() {
260            param_count += 1;
261            where_clauses.push(format!("phone ILIKE ${}", param_count));
262        }
263
264        if filters.last_name.is_some() {
265            param_count += 1;
266            where_clauses.push(format!("last_name ILIKE ${}", param_count));
267        }
268
269        if filters.first_name.is_some() {
270            param_count += 1;
271            where_clauses.push(format!("first_name ILIKE ${}", param_count));
272        }
273
274        let where_clause = if where_clauses.is_empty() {
275            String::new()
276        } else {
277            format!("WHERE {}", where_clauses.join(" AND "))
278        };
279
280        // Validate sort column (whitelist)
281        let allowed_columns = ["last_name", "first_name", "email", "created_at"];
282        let sort_column = page_request.sort_by.as_deref().unwrap_or("last_name");
283
284        if !allowed_columns.contains(&sort_column) {
285            return Err(format!("Invalid sort column: {}", sort_column));
286        }
287
288        // Count total items
289        let count_query = format!("SELECT COUNT(*) FROM owners {}", where_clause);
290        let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
291
292        if let Some(organization_id) = &filters.organization_id {
293            count_query = count_query.bind(organization_id);
294        }
295        if let Some(email) = &filters.email {
296            count_query = count_query.bind(format!("%{}%", email));
297        }
298        if let Some(phone) = &filters.phone {
299            count_query = count_query.bind(format!("%{}%", phone));
300        }
301        if let Some(last_name) = &filters.last_name {
302            count_query = count_query.bind(format!("%{}%", last_name));
303        }
304        if let Some(first_name) = &filters.first_name {
305            count_query = count_query.bind(format!("%{}%", first_name));
306        }
307
308        let total_items = count_query
309            .fetch_one(&self.pool)
310            .await
311            .map_err(|e| format!("Database error: {}", e))?;
312
313        // Fetch paginated data
314        param_count += 1;
315        let limit_param = param_count;
316        param_count += 1;
317        let offset_param = param_count;
318
319        let data_query = format!(
320            "SELECT id, organization_id, user_id, first_name, last_name, email, phone, address, city, postal_code, country, created_at, updated_at \
321             FROM owners {} ORDER BY {} {} LIMIT ${} OFFSET ${}",
322            where_clause,
323            sort_column,
324            page_request.order.to_sql(),
325            limit_param,
326            offset_param
327        );
328
329        let mut data_query = sqlx::query(&data_query);
330
331        if let Some(organization_id) = &filters.organization_id {
332            data_query = data_query.bind(organization_id);
333        }
334        if let Some(email) = &filters.email {
335            data_query = data_query.bind(format!("%{}%", email));
336        }
337        if let Some(phone) = &filters.phone {
338            data_query = data_query.bind(format!("%{}%", phone));
339        }
340        if let Some(last_name) = &filters.last_name {
341            data_query = data_query.bind(format!("%{}%", last_name));
342        }
343        if let Some(first_name) = &filters.first_name {
344            data_query = data_query.bind(format!("%{}%", first_name));
345        }
346
347        data_query = data_query
348            .bind(page_request.limit())
349            .bind(page_request.offset());
350
351        let rows = data_query
352            .fetch_all(&self.pool)
353            .await
354            .map_err(|e| format!("Database error: {}", e))?;
355
356        let owners: Vec<Owner> = rows
357            .iter()
358            .map(|row| Owner {
359                id: row.get("id"),
360                organization_id: row.get("organization_id"),
361                user_id: row.get("user_id"),
362                first_name: row.get("first_name"),
363                last_name: row.get("last_name"),
364                email: row.get("email"),
365                phone: row.get("phone"),
366                address: row.get("address"),
367                city: row.get("city"),
368                postal_code: row.get("postal_code"),
369                country: row.get("country"),
370                created_at: row.get("created_at"),
371                updated_at: row.get("updated_at"),
372            })
373            .collect();
374
375        Ok((owners, total_items))
376    }
377
378    async fn update(&self, owner: &Owner) -> Result<Owner, String> {
379        sqlx::query(
380            r#"
381            UPDATE owners
382            SET first_name = $2, last_name = $3, email = $4, phone = $5, address = $6, city = $7, postal_code = $8, country = $9, updated_at = $10
383            WHERE id = $1
384            "#,
385        )
386        .bind(owner.id)
387        .bind(&owner.first_name)
388        .bind(&owner.last_name)
389        .bind(&owner.email)
390        .bind(&owner.phone)
391        .bind(&owner.address)
392        .bind(&owner.city)
393        .bind(&owner.postal_code)
394        .bind(&owner.country)
395        .bind(owner.updated_at)
396        .execute(&self.pool)
397        .await
398        .map_err(|e| format!("Database error: {}", e))?;
399
400        Ok(owner.clone())
401    }
402
403    async fn delete(&self, id: Uuid) -> Result<bool, String> {
404        let result = sqlx::query("DELETE FROM owners WHERE id = $1")
405            .bind(id)
406            .execute(&self.pool)
407            .await
408            .map_err(|e| format!("Database error: {}", e))?;
409
410        Ok(result.rows_affected() > 0)
411    }
412
413    async fn set_user_link(&self, owner_id: Uuid, user_id: Option<Uuid>) -> Result<bool, String> {
414        let result =
415            sqlx::query("UPDATE owners SET user_id = $1, updated_at = NOW() WHERE id = $2")
416                .bind(user_id)
417                .bind(owner_id)
418                .execute(&self.pool)
419                .await
420                .map_err(|e| format!("Database error: {}", e))?;
421
422        Ok(result.rows_affected() > 0)
423    }
424}