koprogo_api/infrastructure/database/repositories/
unit_repository_impl.rs

1use crate::application::dto::{PageRequest, UnitFilters};
2use crate::application::ports::UnitRepository;
3use crate::domain::entities::{Unit, UnitType};
4use crate::infrastructure::database::pool::DbPool;
5use async_trait::async_trait;
6use sqlx::Row;
7use uuid::Uuid;
8
9pub struct PostgresUnitRepository {
10    pool: DbPool,
11}
12
13impl PostgresUnitRepository {
14    pub fn new(pool: DbPool) -> Self {
15        Self { pool }
16    }
17}
18
19#[async_trait]
20impl UnitRepository for PostgresUnitRepository {
21    async fn create(&self, unit: &Unit) -> Result<Unit, String> {
22        let unit_type_str = match unit.unit_type {
23            UnitType::Apartment => "apartment",
24            UnitType::Parking => "parking",
25            UnitType::Cellar => "cellar",
26            UnitType::Commercial => "commercial",
27            UnitType::Other => "other",
28        };
29
30        sqlx::query(
31            r#"
32            INSERT INTO units (id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at)
33            VALUES ($1, $2, $3, $4, $5::unit_type, $6, $7, $8, $9, $10, $11)
34            "#,
35        )
36        .bind(unit.id)
37        .bind(unit.organization_id)
38        .bind(unit.building_id)
39        .bind(&unit.unit_number)
40        .bind(unit_type_str)
41        .bind(unit.floor)
42        .bind(unit.surface_area)
43        .bind(unit.quota)
44        .bind(unit.owner_id)
45        .bind(unit.created_at)
46        .bind(unit.updated_at)
47        .execute(&self.pool)
48        .await
49        .map_err(|e| format!("Database error: {}", e))?;
50
51        Ok(unit.clone())
52    }
53
54    async fn find_by_id(&self, id: Uuid) -> Result<Option<Unit>, String> {
55        let row = sqlx::query(
56            r#"
57            SELECT id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at
58            FROM units
59            WHERE id = $1
60            "#,
61        )
62        .bind(id)
63        .fetch_optional(&self.pool)
64        .await
65        .map_err(|e| format!("Database error: {}", e))?;
66
67        Ok(row.map(|row| {
68            let unit_type_str: String = row.get("unit_type");
69            let unit_type = match unit_type_str.as_str() {
70                "apartment" => UnitType::Apartment,
71                "parking" => UnitType::Parking,
72                "cellar" => UnitType::Cellar,
73                "commercial" => UnitType::Commercial,
74                _ => UnitType::Other,
75            };
76
77            Unit {
78                id: row.get("id"),
79                organization_id: row.get("organization_id"),
80                building_id: row.get("building_id"),
81                unit_number: row.get("unit_number"),
82                unit_type,
83                floor: row.get("floor"),
84                surface_area: row.get("surface_area"),
85                quota: row.get("quota"),
86                owner_id: row.get("owner_id"),
87                created_at: row.get("created_at"),
88                updated_at: row.get("updated_at"),
89            }
90        }))
91    }
92
93    async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Unit>, String> {
94        let rows = sqlx::query(
95            r#"
96            SELECT id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at
97            FROM units
98            WHERE building_id = $1
99            ORDER BY unit_number
100            "#,
101        )
102        .bind(building_id)
103        .fetch_all(&self.pool)
104        .await
105        .map_err(|e| format!("Database error: {}", e))?;
106
107        Ok(rows
108            .iter()
109            .map(|row| {
110                let unit_type_str: String = row.get("unit_type");
111                let unit_type = match unit_type_str.as_str() {
112                    "apartment" => UnitType::Apartment,
113                    "parking" => UnitType::Parking,
114                    "cellar" => UnitType::Cellar,
115                    "commercial" => UnitType::Commercial,
116                    _ => UnitType::Other,
117                };
118
119                Unit {
120                    id: row.get("id"),
121                    organization_id: row.get("organization_id"),
122                    building_id: row.get("building_id"),
123                    unit_number: row.get("unit_number"),
124                    unit_type,
125                    floor: row.get("floor"),
126                    surface_area: row.get("surface_area"),
127                    quota: row.get("quota"),
128                    owner_id: row.get("owner_id"),
129                    created_at: row.get("created_at"),
130                    updated_at: row.get("updated_at"),
131                }
132            })
133            .collect())
134    }
135
136    async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<Unit>, String> {
137        let rows = sqlx::query(
138            r#"
139            SELECT id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at
140            FROM units
141            WHERE owner_id = $1
142            ORDER BY unit_number
143            "#,
144        )
145        .bind(owner_id)
146        .fetch_all(&self.pool)
147        .await
148        .map_err(|e| format!("Database error: {}", e))?;
149
150        Ok(rows
151            .iter()
152            .map(|row| {
153                let unit_type_str: String = row.get("unit_type");
154                let unit_type = match unit_type_str.as_str() {
155                    "apartment" => UnitType::Apartment,
156                    "parking" => UnitType::Parking,
157                    "cellar" => UnitType::Cellar,
158                    "commercial" => UnitType::Commercial,
159                    _ => UnitType::Other,
160                };
161
162                Unit {
163                    id: row.get("id"),
164                    organization_id: row.get("organization_id"),
165                    building_id: row.get("building_id"),
166                    unit_number: row.get("unit_number"),
167                    unit_type,
168                    floor: row.get("floor"),
169                    surface_area: row.get("surface_area"),
170                    quota: row.get("quota"),
171                    owner_id: row.get("owner_id"),
172                    created_at: row.get("created_at"),
173                    updated_at: row.get("updated_at"),
174                }
175            })
176            .collect())
177    }
178
179    async fn find_all_paginated(
180        &self,
181        page_request: &PageRequest,
182        filters: &UnitFilters,
183    ) -> Result<(Vec<Unit>, i64), String> {
184        // Validate page request
185        page_request.validate()?;
186
187        // Build WHERE clause dynamically
188        let mut where_clauses = Vec::new();
189        let mut param_count = 0;
190
191        if filters.building_id.is_some() {
192            param_count += 1;
193            where_clauses.push(format!("building_id = ${}", param_count));
194        }
195
196        if filters.floor.is_some() {
197            param_count += 1;
198            where_clauses.push(format!("floor = ${}", param_count));
199        }
200
201        if let Some(has_owner) = filters.has_owner {
202            if has_owner {
203                where_clauses.push("owner_id IS NOT NULL".to_string());
204            } else {
205                where_clauses.push("owner_id IS NULL".to_string());
206            }
207        }
208
209        if filters.min_area.is_some() {
210            param_count += 1;
211            where_clauses.push(format!("surface_area >= ${}", param_count));
212        }
213
214        if filters.max_area.is_some() {
215            param_count += 1;
216            where_clauses.push(format!("surface_area <= ${}", param_count));
217        }
218
219        let where_clause = if where_clauses.is_empty() {
220            String::new()
221        } else {
222            format!("WHERE {}", where_clauses.join(" AND "))
223        };
224
225        // Validate sort column (whitelist)
226        let allowed_columns = ["unit_number", "floor", "surface_area", "created_at"];
227        let sort_column = page_request.sort_by.as_deref().unwrap_or("unit_number");
228
229        if !allowed_columns.contains(&sort_column) {
230            return Err(format!("Invalid sort column: {}", sort_column));
231        }
232
233        // Count total items
234        let count_query = format!("SELECT COUNT(*) FROM units {}", where_clause);
235        let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
236
237        if let Some(building_id) = filters.building_id {
238            count_query = count_query.bind(building_id);
239        }
240        if let Some(floor) = filters.floor {
241            count_query = count_query.bind(floor);
242        }
243        if let Some(min_area) = filters.min_area {
244            count_query = count_query.bind(min_area);
245        }
246        if let Some(max_area) = filters.max_area {
247            count_query = count_query.bind(max_area);
248        }
249
250        let total_items = count_query
251            .fetch_one(&self.pool)
252            .await
253            .map_err(|e| format!("Database error: {}", e))?;
254
255        // Fetch paginated data
256        param_count += 1;
257        let limit_param = param_count;
258        param_count += 1;
259        let offset_param = param_count;
260
261        let data_query = format!(
262            "SELECT id, organization_id, building_id, unit_number, unit_type, floor, surface_area, quota, owner_id, created_at, updated_at \
263             FROM units {} ORDER BY {} {} LIMIT ${} OFFSET ${}",
264            where_clause,
265            sort_column,
266            page_request.order.to_sql(),
267            limit_param,
268            offset_param
269        );
270
271        let mut data_query = sqlx::query(&data_query);
272
273        if let Some(building_id) = filters.building_id {
274            data_query = data_query.bind(building_id);
275        }
276        if let Some(floor) = filters.floor {
277            data_query = data_query.bind(floor);
278        }
279        if let Some(min_area) = filters.min_area {
280            data_query = data_query.bind(min_area);
281        }
282        if let Some(max_area) = filters.max_area {
283            data_query = data_query.bind(max_area);
284        }
285
286        data_query = data_query
287            .bind(page_request.limit())
288            .bind(page_request.offset());
289
290        let rows = data_query
291            .fetch_all(&self.pool)
292            .await
293            .map_err(|e| format!("Database error: {}", e))?;
294
295        let units: Vec<Unit> = rows
296            .iter()
297            .map(|row| {
298                // Try to get as String first (for enum types from PostgreSQL)
299                let unit_type_str: String = row
300                    .try_get("unit_type")
301                    .unwrap_or_else(|_| "apartment".to_string());
302                let unit_type = match unit_type_str.as_str() {
303                    "apartment" => UnitType::Apartment,
304                    "parking" => UnitType::Parking,
305                    "cellar" => UnitType::Cellar,
306                    "commercial" => UnitType::Commercial,
307                    _ => UnitType::Other,
308                };
309
310                Unit {
311                    id: row.get("id"),
312                    organization_id: row.get("organization_id"),
313                    building_id: row.get("building_id"),
314                    unit_number: row.get("unit_number"),
315                    unit_type,
316                    floor: row.get("floor"),
317                    surface_area: row.get("surface_area"),
318                    quota: row.get("quota"),
319                    owner_id: row.get("owner_id"),
320                    created_at: row.get("created_at"),
321                    updated_at: row.get("updated_at"),
322                }
323            })
324            .collect();
325
326        Ok((units, total_items))
327    }
328
329    async fn update(&self, unit: &Unit) -> Result<Unit, String> {
330        sqlx::query(
331            r#"
332            UPDATE units
333            SET owner_id = $2, updated_at = $3
334            WHERE id = $1
335            "#,
336        )
337        .bind(unit.id)
338        .bind(unit.owner_id)
339        .bind(unit.updated_at)
340        .execute(&self.pool)
341        .await
342        .map_err(|e| format!("Database error: {}", e))?;
343
344        Ok(unit.clone())
345    }
346
347    async fn delete(&self, id: Uuid) -> Result<bool, String> {
348        let result = sqlx::query("DELETE FROM units WHERE id = $1")
349            .bind(id)
350            .execute(&self.pool)
351            .await
352            .map_err(|e| format!("Database error: {}", e))?;
353
354        Ok(result.rows_affected() > 0)
355    }
356}