koprogo_api/infrastructure/database/repositories/
building_repository_impl.rs1use 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 page_request.validate()?;
111
112 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 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 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 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}