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 page_request.validate()?;
186
187 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 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 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 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 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}