koprogo_api/infrastructure/database/repositories/
building_repository_impl.rs

1use crate::application::dto::{BuildingFilters, PageRequest};
2use crate::application::ports::BuildingRepository;
3use crate::domain::entities::Building;
4use crate::infrastructure::database::pool::DbPool;
5use async_trait::async_trait;
6use sqlx::Row;
7use uuid::Uuid;
8
9pub struct PostgresBuildingRepository {
10    pool: DbPool,
11}
12
13impl PostgresBuildingRepository {
14    pub fn new(pool: DbPool) -> Self {
15        Self { pool }
16    }
17}
18
19#[async_trait]
20impl BuildingRepository for PostgresBuildingRepository {
21    async fn create(&self, building: &Building) -> Result<Building, String> {
22        sqlx::query(
23            r#"
24            INSERT INTO buildings (id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at)
25            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
26            "#,
27        )
28        .bind(building.id)
29        .bind(building.organization_id)
30        .bind(&building.name)
31        .bind(&building.address)
32        .bind(&building.city)
33        .bind(&building.postal_code)
34        .bind(&building.country)
35        .bind(building.total_units)
36        .bind(building.construction_year)
37        .bind(building.created_at)
38        .bind(building.updated_at)
39        .execute(&self.pool)
40        .await
41        .map_err(|e| format!("Database error: {}", e))?;
42
43        Ok(building.clone())
44    }
45
46    async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String> {
47        let row = sqlx::query(
48            r#"
49            SELECT id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at
50            FROM buildings
51            WHERE id = $1
52            "#,
53        )
54        .bind(id)
55        .fetch_optional(&self.pool)
56        .await
57        .map_err(|e| format!("Database error: {}", e))?;
58
59        Ok(row.map(|row| Building {
60            id: row.get("id"),
61            organization_id: row.get("organization_id"),
62            name: row.get("name"),
63            address: row.get("address"),
64            city: row.get("city"),
65            postal_code: row.get("postal_code"),
66            country: row.get("country"),
67            total_units: row.get("total_units"),
68            construction_year: row.get("construction_year"),
69            created_at: row.get("created_at"),
70            updated_at: row.get("updated_at"),
71        }))
72    }
73
74    async fn find_all(&self) -> Result<Vec<Building>, String> {
75        let rows = sqlx::query(
76            r#"
77            SELECT id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at
78            FROM buildings
79            ORDER BY created_at DESC
80            "#,
81        )
82        .fetch_all(&self.pool)
83        .await
84        .map_err(|e| format!("Database error: {}", e))?;
85
86        Ok(rows
87            .iter()
88            .map(|row| Building {
89                id: row.get("id"),
90                organization_id: row.get("organization_id"),
91                name: row.get("name"),
92                address: row.get("address"),
93                city: row.get("city"),
94                postal_code: row.get("postal_code"),
95                country: row.get("country"),
96                total_units: row.get("total_units"),
97                construction_year: row.get("construction_year"),
98                created_at: row.get("created_at"),
99                updated_at: row.get("updated_at"),
100            })
101            .collect())
102    }
103
104    async fn find_all_paginated(
105        &self,
106        page_request: &PageRequest,
107        filters: &BuildingFilters,
108    ) -> Result<(Vec<Building>, i64), String> {
109        // Validate page request
110        page_request.validate()?;
111
112        // Build WHERE clause dynamically
113        let mut where_clauses = Vec::new();
114        let mut param_count = 0;
115
116        if filters.organization_id.is_some() {
117            param_count += 1;
118            where_clauses.push(format!("organization_id = ${}", param_count));
119        }
120
121        if filters.city.is_some() {
122            param_count += 1;
123            where_clauses.push(format!("city ILIKE ${}", param_count));
124        }
125
126        if filters.construction_year.is_some() {
127            param_count += 1;
128            where_clauses.push(format!("construction_year = ${}", param_count));
129        }
130
131        if filters.min_units.is_some() {
132            param_count += 1;
133            where_clauses.push(format!("total_units >= ${}", param_count));
134        }
135
136        if filters.max_units.is_some() {
137            param_count += 1;
138            where_clauses.push(format!("total_units <= ${}", param_count));
139        }
140
141        let where_clause = if where_clauses.is_empty() {
142            String::new()
143        } else {
144            format!("WHERE {}", where_clauses.join(" AND "))
145        };
146
147        // Validate sort column (whitelist)
148        let allowed_columns = [
149            "name",
150            "created_at",
151            "total_units",
152            "city",
153            "construction_year",
154        ];
155        let sort_column = page_request.sort_by.as_deref().unwrap_or("created_at");
156
157        if !allowed_columns.contains(&sort_column) {
158            return Err(format!("Invalid sort column: {}", sort_column));
159        }
160
161        // Count total items
162        let count_query = format!("SELECT COUNT(*) FROM buildings {}", where_clause);
163        let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
164
165        if let Some(org_id) = filters.organization_id {
166            count_query = count_query.bind(org_id);
167        }
168        if let Some(city) = &filters.city {
169            count_query = count_query.bind(format!("%{}%", city));
170        }
171        if let Some(year) = filters.construction_year {
172            count_query = count_query.bind(year);
173        }
174        if let Some(min) = filters.min_units {
175            count_query = count_query.bind(min);
176        }
177        if let Some(max) = filters.max_units {
178            count_query = count_query.bind(max);
179        }
180
181        let total_items = count_query
182            .fetch_one(&self.pool)
183            .await
184            .map_err(|e| format!("Database error: {}", e))?;
185
186        // Fetch paginated data
187        param_count += 1;
188        let limit_param = param_count;
189        param_count += 1;
190        let offset_param = param_count;
191
192        let data_query = format!(
193            "SELECT id, organization_id, name, address, city, postal_code, country, total_units, construction_year, created_at, updated_at \
194             FROM buildings {} ORDER BY {} {} LIMIT ${} OFFSET ${}",
195            where_clause,
196            sort_column,
197            page_request.order.to_sql(),
198            limit_param,
199            offset_param
200        );
201
202        let mut data_query = sqlx::query(&data_query);
203
204        if let Some(org_id) = filters.organization_id {
205            data_query = data_query.bind(org_id);
206        }
207        if let Some(city) = &filters.city {
208            data_query = data_query.bind(format!("%{}%", city));
209        }
210        if let Some(year) = filters.construction_year {
211            data_query = data_query.bind(year);
212        }
213        if let Some(min) = filters.min_units {
214            data_query = data_query.bind(min);
215        }
216        if let Some(max) = filters.max_units {
217            data_query = data_query.bind(max);
218        }
219
220        data_query = data_query
221            .bind(page_request.limit())
222            .bind(page_request.offset());
223
224        let rows = data_query
225            .fetch_all(&self.pool)
226            .await
227            .map_err(|e| format!("Database error: {}", e))?;
228
229        let buildings: Vec<Building> = rows
230            .iter()
231            .map(|row| Building {
232                id: row.get("id"),
233                organization_id: row.get("organization_id"),
234                name: row.get("name"),
235                address: row.get("address"),
236                city: row.get("city"),
237                postal_code: row.get("postal_code"),
238                country: row.get("country"),
239                total_units: row.get("total_units"),
240                construction_year: row.get("construction_year"),
241                created_at: row.get("created_at"),
242                updated_at: row.get("updated_at"),
243            })
244            .collect();
245
246        Ok((buildings, total_items))
247    }
248
249    async fn update(&self, building: &Building) -> Result<Building, String> {
250        sqlx::query(
251            r#"
252            UPDATE buildings
253            SET name = $2, address = $3, city = $4, postal_code = $5, updated_at = $6
254            WHERE id = $1
255            "#,
256        )
257        .bind(building.id)
258        .bind(&building.name)
259        .bind(&building.address)
260        .bind(&building.city)
261        .bind(&building.postal_code)
262        .bind(building.updated_at)
263        .execute(&self.pool)
264        .await
265        .map_err(|e| format!("Database error: {}", e))?;
266
267        Ok(building.clone())
268    }
269
270    async fn delete(&self, id: Uuid) -> Result<bool, String> {
271        let result = sqlx::query("DELETE FROM buildings WHERE id = $1")
272            .bind(id)
273            .execute(&self.pool)
274            .await
275            .map_err(|e| format!("Database error: {}", e))?;
276
277        Ok(result.rows_affected() > 0)
278    }
279}