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, total_tantiemes, construction_year, syndic_name, syndic_email, syndic_phone, syndic_address, syndic_office_hours, syndic_emergency_contact, slug, created_at, updated_at)
25 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
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.total_tantiemes)
37 .bind(building.construction_year)
38 .bind(&building.syndic_name)
39 .bind(&building.syndic_email)
40 .bind(&building.syndic_phone)
41 .bind(&building.syndic_address)
42 .bind(&building.syndic_office_hours)
43 .bind(&building.syndic_emergency_contact)
44 .bind(&building.slug)
45 .bind(building.created_at)
46 .bind(building.updated_at)
47 .execute(&self.pool)
48 .await
49 .map_err(|e| format!("Database error: {}", e))?;
50
51 Ok(building.clone())
52 }
53
54 async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String> {
55 let row = sqlx::query(
56 r#"
57 SELECT id, organization_id, name, address, city, postal_code, country, total_units, total_tantiemes, construction_year, syndic_name, syndic_email, syndic_phone, syndic_address, syndic_office_hours, syndic_emergency_contact, slug, created_at, updated_at
58 FROM buildings
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| Building {
68 id: row.get("id"),
69 organization_id: row.get("organization_id"),
70 name: row.get("name"),
71 address: row.get("address"),
72 city: row.get("city"),
73 postal_code: row.get("postal_code"),
74 country: row.get("country"),
75 total_units: row.get("total_units"),
76 total_tantiemes: row.get("total_tantiemes"),
77 construction_year: row.get("construction_year"),
78 syndic_name: row.get("syndic_name"),
79 syndic_email: row.get("syndic_email"),
80 syndic_phone: row.get("syndic_phone"),
81 syndic_address: row.get("syndic_address"),
82 syndic_office_hours: row.get("syndic_office_hours"),
83 syndic_emergency_contact: row.get("syndic_emergency_contact"),
84 slug: row.get("slug"),
85 created_at: row.get("created_at"),
86 updated_at: row.get("updated_at"),
87 }))
88 }
89
90 async fn find_all(&self) -> Result<Vec<Building>, String> {
91 let rows = sqlx::query(
92 r#"
93 SELECT id, organization_id, name, address, city, postal_code, country, total_units, total_tantiemes, construction_year, syndic_name, syndic_email, syndic_phone, syndic_address, syndic_office_hours, syndic_emergency_contact, slug, created_at, updated_at
94 FROM buildings
95 ORDER BY created_at DESC
96 "#,
97 )
98 .fetch_all(&self.pool)
99 .await
100 .map_err(|e| format!("Database error: {}", e))?;
101
102 Ok(rows
103 .iter()
104 .map(|row| Building {
105 id: row.get("id"),
106 organization_id: row.get("organization_id"),
107 name: row.get("name"),
108 address: row.get("address"),
109 city: row.get("city"),
110 postal_code: row.get("postal_code"),
111 country: row.get("country"),
112 total_units: row.get("total_units"),
113 total_tantiemes: row.get("total_tantiemes"),
114 construction_year: row.get("construction_year"),
115 syndic_name: row.get("syndic_name"),
116 syndic_email: row.get("syndic_email"),
117 syndic_phone: row.get("syndic_phone"),
118 syndic_address: row.get("syndic_address"),
119 syndic_office_hours: row.get("syndic_office_hours"),
120 syndic_emergency_contact: row.get("syndic_emergency_contact"),
121 slug: row.get("slug"),
122 created_at: row.get("created_at"),
123 updated_at: row.get("updated_at"),
124 })
125 .collect())
126 }
127
128 async fn find_all_paginated(
129 &self,
130 page_request: &PageRequest,
131 filters: &BuildingFilters,
132 ) -> Result<(Vec<Building>, i64), String> {
133 page_request.validate()?;
135
136 let mut where_clauses = Vec::new();
138 let mut param_count = 0;
139
140 if filters.organization_id.is_some() {
141 param_count += 1;
142 where_clauses.push(format!("organization_id = ${}", param_count));
143 }
144
145 if filters.city.is_some() {
146 param_count += 1;
147 where_clauses.push(format!("city ILIKE ${}", param_count));
148 }
149
150 if filters.construction_year.is_some() {
151 param_count += 1;
152 where_clauses.push(format!("construction_year = ${}", param_count));
153 }
154
155 if filters.min_units.is_some() {
156 param_count += 1;
157 where_clauses.push(format!("total_units >= ${}", param_count));
158 }
159
160 if filters.max_units.is_some() {
161 param_count += 1;
162 where_clauses.push(format!("total_units <= ${}", param_count));
163 }
164
165 if filters.owner_user_id.is_some() {
167 param_count += 1;
168 where_clauses.push(format!(
169 "id IN (SELECT DISTINCT u.building_id FROM units u \
170 INNER JOIN unit_owners uo ON uo.unit_id = u.id \
171 INNER JOIN owners o ON o.id = uo.owner_id \
172 WHERE o.user_id = ${} AND uo.end_date IS NULL)",
173 param_count
174 ));
175 }
176
177 let where_clause = if where_clauses.is_empty() {
178 String::new()
179 } else {
180 format!("WHERE {}", where_clauses.join(" AND "))
181 };
182
183 let allowed_columns = [
185 "name",
186 "created_at",
187 "total_units",
188 "city",
189 "construction_year",
190 ];
191 let sort_column = page_request.sort_by.as_deref().unwrap_or("created_at");
192
193 if !allowed_columns.contains(&sort_column) {
194 return Err(format!("Invalid sort column: {}", sort_column));
195 }
196
197 let count_query = format!("SELECT COUNT(*) FROM buildings {}", where_clause);
199 let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
200
201 if let Some(org_id) = filters.organization_id {
202 count_query = count_query.bind(org_id);
203 }
204 if let Some(city) = &filters.city {
205 count_query = count_query.bind(format!("%{}%", city));
206 }
207 if let Some(year) = filters.construction_year {
208 count_query = count_query.bind(year);
209 }
210 if let Some(min) = filters.min_units {
211 count_query = count_query.bind(min);
212 }
213 if let Some(max) = filters.max_units {
214 count_query = count_query.bind(max);
215 }
216 if let Some(owner_id) = filters.owner_user_id {
217 count_query = count_query.bind(owner_id);
218 }
219
220 let total_items = count_query
221 .fetch_one(&self.pool)
222 .await
223 .map_err(|e| format!("Database error: {}", e))?;
224
225 param_count += 1;
227 let limit_param = param_count;
228 param_count += 1;
229 let offset_param = param_count;
230
231 let data_query = format!(
232 "SELECT id, organization_id, name, address, city, postal_code, country, total_units, total_tantiemes, construction_year, syndic_name, syndic_email, syndic_phone, syndic_address, syndic_office_hours, syndic_emergency_contact, slug, created_at, updated_at \
233 FROM buildings {} ORDER BY {} {} LIMIT ${} OFFSET ${}",
234 where_clause,
235 sort_column,
236 page_request.order.to_sql(),
237 limit_param,
238 offset_param
239 );
240
241 let mut data_query = sqlx::query(&data_query);
242
243 if let Some(org_id) = filters.organization_id {
244 data_query = data_query.bind(org_id);
245 }
246 if let Some(city) = &filters.city {
247 data_query = data_query.bind(format!("%{}%", city));
248 }
249 if let Some(year) = filters.construction_year {
250 data_query = data_query.bind(year);
251 }
252 if let Some(min) = filters.min_units {
253 data_query = data_query.bind(min);
254 }
255 if let Some(max) = filters.max_units {
256 data_query = data_query.bind(max);
257 }
258 if let Some(owner_id) = filters.owner_user_id {
259 data_query = data_query.bind(owner_id);
260 }
261
262 data_query = data_query
263 .bind(page_request.limit())
264 .bind(page_request.offset());
265
266 let rows = data_query
267 .fetch_all(&self.pool)
268 .await
269 .map_err(|e| format!("Database error: {}", e))?;
270
271 let buildings: Vec<Building> = rows
272 .iter()
273 .map(|row| Building {
274 id: row.get("id"),
275 organization_id: row.get("organization_id"),
276 name: row.get("name"),
277 address: row.get("address"),
278 city: row.get("city"),
279 postal_code: row.get("postal_code"),
280 country: row.get("country"),
281 total_units: row.get("total_units"),
282 total_tantiemes: row.get("total_tantiemes"),
283 construction_year: row.get("construction_year"),
284 syndic_name: row.get("syndic_name"),
285 syndic_email: row.get("syndic_email"),
286 syndic_phone: row.get("syndic_phone"),
287 syndic_address: row.get("syndic_address"),
288 syndic_office_hours: row.get("syndic_office_hours"),
289 syndic_emergency_contact: row.get("syndic_emergency_contact"),
290 slug: row.get("slug"),
291 created_at: row.get("created_at"),
292 updated_at: row.get("updated_at"),
293 })
294 .collect();
295
296 Ok((buildings, total_items))
297 }
298
299 async fn update(&self, building: &Building) -> Result<Building, String> {
300 sqlx::query(
301 r#"
302 UPDATE buildings
303 SET organization_id = $2, name = $3, address = $4, city = $5, postal_code = $6, country = $7, total_units = $8, total_tantiemes = $9, construction_year = $10, syndic_name = $11, syndic_email = $12, syndic_phone = $13, syndic_address = $14, syndic_office_hours = $15, syndic_emergency_contact = $16, slug = $17, updated_at = $18
304 WHERE id = $1
305 "#,
306 )
307 .bind(building.id)
308 .bind(building.organization_id)
309 .bind(&building.name)
310 .bind(&building.address)
311 .bind(&building.city)
312 .bind(&building.postal_code)
313 .bind(&building.country)
314 .bind(building.total_units)
315 .bind(building.total_tantiemes)
316 .bind(building.construction_year)
317 .bind(&building.syndic_name)
318 .bind(&building.syndic_email)
319 .bind(&building.syndic_phone)
320 .bind(&building.syndic_address)
321 .bind(&building.syndic_office_hours)
322 .bind(&building.syndic_emergency_contact)
323 .bind(&building.slug)
324 .bind(building.updated_at)
325 .execute(&self.pool)
326 .await
327 .map_err(|e| format!("Database error: {}", e))?;
328
329 Ok(building.clone())
330 }
331
332 async fn delete(&self, id: Uuid) -> Result<bool, String> {
333 let result = sqlx::query("DELETE FROM buildings WHERE id = $1")
334 .bind(id)
335 .execute(&self.pool)
336 .await
337 .map_err(|e| format!("Database error: {}", e))?;
338
339 Ok(result.rows_affected() > 0)
340 }
341
342 async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String> {
343 let row = sqlx::query(
344 r#"
345 SELECT id, organization_id, name, address, city, postal_code, country, total_units, total_tantiemes, construction_year, syndic_name, syndic_email, syndic_phone, syndic_address, syndic_office_hours, syndic_emergency_contact, slug, created_at, updated_at
346 FROM buildings
347 WHERE slug = $1
348 "#,
349 )
350 .bind(slug)
351 .fetch_optional(&self.pool)
352 .await
353 .map_err(|e| format!("Database error: {}", e))?;
354
355 Ok(row.map(|row| Building {
356 id: row.get("id"),
357 organization_id: row.get("organization_id"),
358 name: row.get("name"),
359 address: row.get("address"),
360 city: row.get("city"),
361 postal_code: row.get("postal_code"),
362 country: row.get("country"),
363 total_units: row.get("total_units"),
364 total_tantiemes: row.get("total_tantiemes"),
365 construction_year: row.get("construction_year"),
366 syndic_name: row.get("syndic_name"),
367 syndic_email: row.get("syndic_email"),
368 syndic_phone: row.get("syndic_phone"),
369 syndic_address: row.get("syndic_address"),
370 syndic_office_hours: row.get("syndic_office_hours"),
371 syndic_emergency_contact: row.get("syndic_emergency_contact"),
372 slug: row.get("slug"),
373 created_at: row.get("created_at"),
374 updated_at: row.get("updated_at"),
375 }))
376 }
377}