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, acp_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.acp_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, acp_id, building_id, unit_number, unit_type::text AS 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 acp_id: row.get("acp_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, acp_id, building_id, unit_number, unit_type::text AS 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 acp_id: row.get("acp_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, acp_id, building_id, unit_number, unit_type::text AS 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 acp_id: row.get("acp_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.organization_id.is_some() {
194 param_count += 1;
195 where_clauses.push(format!(
196 "acp_id IN (SELECT id FROM acps WHERE organization_id = ${})",
197 param_count
198 ));
199 }
200
201 if filters.acp_id.is_some() {
203 param_count += 1;
204 where_clauses.push(format!("acp_id = ${}", param_count));
205 }
206
207 if filters.building_id.is_some() {
208 param_count += 1;
209 where_clauses.push(format!("building_id = ${}", param_count));
210 }
211
212 if filters.floor.is_some() {
213 param_count += 1;
214 where_clauses.push(format!("floor = ${}", param_count));
215 }
216
217 if let Some(has_owner) = filters.has_owner {
218 if has_owner {
219 where_clauses.push("owner_id IS NOT NULL".to_string());
220 } else {
221 where_clauses.push("owner_id IS NULL".to_string());
222 }
223 }
224
225 if filters.min_area.is_some() {
226 param_count += 1;
227 where_clauses.push(format!("surface_area >= ${}", param_count));
228 }
229
230 if filters.max_area.is_some() {
231 param_count += 1;
232 where_clauses.push(format!("surface_area <= ${}", param_count));
233 }
234
235 let where_clause = if where_clauses.is_empty() {
236 String::new()
237 } else {
238 format!("WHERE {}", where_clauses.join(" AND "))
239 };
240
241 let allowed_columns = ["unit_number", "floor", "surface_area", "created_at"];
243 let sort_column = page_request.sort_by.as_deref().unwrap_or("unit_number");
244
245 if !allowed_columns.contains(&sort_column) {
246 return Err(format!("Invalid sort column: {}", sort_column));
247 }
248
249 let count_query = format!("SELECT COUNT(*) FROM units {}", where_clause);
251 let mut count_query = sqlx::query_scalar::<_, i64>(&count_query);
252
253 if let Some(org_id) = filters.organization_id {
256 count_query = count_query.bind(org_id);
257 }
258 if let Some(acp_id) = filters.acp_id {
259 count_query = count_query.bind(acp_id);
260 }
261 if let Some(building_id) = filters.building_id {
262 count_query = count_query.bind(building_id);
263 }
264 if let Some(floor) = filters.floor {
265 count_query = count_query.bind(floor);
266 }
267 if let Some(min_area) = filters.min_area {
268 count_query = count_query.bind(min_area);
269 }
270 if let Some(max_area) = filters.max_area {
271 count_query = count_query.bind(max_area);
272 }
273
274 let total_items = count_query
275 .fetch_one(&self.pool)
276 .await
277 .map_err(|e| format!("Database error: {}", e))?;
278
279 param_count += 1;
281 let limit_param = param_count;
282 param_count += 1;
283 let offset_param = param_count;
284
285 let data_query = format!(
286 "SELECT id, acp_id, building_id, unit_number, unit_type::text AS unit_type, floor, surface_area, quota, owner_id, created_at, updated_at \
287 FROM units {} ORDER BY {} {} LIMIT ${} OFFSET ${}",
288 where_clause,
289 sort_column,
290 page_request.order.to_sql(),
291 limit_param,
292 offset_param
293 );
294
295 let mut data_query = sqlx::query(&data_query);
296
297 if let Some(org_id) = filters.organization_id {
300 data_query = data_query.bind(org_id);
301 }
302 if let Some(acp_id) = filters.acp_id {
303 data_query = data_query.bind(acp_id);
304 }
305 if let Some(building_id) = filters.building_id {
306 data_query = data_query.bind(building_id);
307 }
308 if let Some(floor) = filters.floor {
309 data_query = data_query.bind(floor);
310 }
311 if let Some(min_area) = filters.min_area {
312 data_query = data_query.bind(min_area);
313 }
314 if let Some(max_area) = filters.max_area {
315 data_query = data_query.bind(max_area);
316 }
317
318 data_query = data_query
319 .bind(page_request.limit())
320 .bind(page_request.offset());
321
322 let rows = data_query
323 .fetch_all(&self.pool)
324 .await
325 .map_err(|e| format!("Database error: {}", e))?;
326
327 let units: Vec<Unit> = rows
328 .iter()
329 .map(|row| {
330 let unit_type_str: String = row
332 .try_get("unit_type")
333 .unwrap_or_else(|_| "apartment".to_string());
334 let unit_type = match unit_type_str.as_str() {
335 "apartment" => UnitType::Apartment,
336 "parking" => UnitType::Parking,
337 "cellar" => UnitType::Cellar,
338 "commercial" => UnitType::Commercial,
339 _ => UnitType::Other,
340 };
341
342 Unit {
343 id: row.get("id"),
344 acp_id: row.get("acp_id"),
345 building_id: row.get("building_id"),
346 unit_number: row.get("unit_number"),
347 unit_type,
348 floor: row.get("floor"),
349 surface_area: row.get("surface_area"),
350 quota: row.get("quota"),
351 owner_id: row.get("owner_id"),
352 created_at: row.get("created_at"),
353 updated_at: row.get("updated_at"),
354 }
355 })
356 .collect();
357
358 Ok((units, total_items))
359 }
360
361 async fn update(&self, unit: &Unit) -> Result<Unit, String> {
362 let unit_type_str = match unit.unit_type {
363 UnitType::Apartment => "apartment",
364 UnitType::Parking => "parking",
365 UnitType::Cellar => "cellar",
366 UnitType::Commercial => "commercial",
367 UnitType::Other => "other",
368 };
369
370 sqlx::query(
371 r#"
372 UPDATE units
373 SET unit_number = $2,
374 unit_type = $3::unit_type,
375 floor = $4,
376 surface_area = $5,
377 quota = $6,
378 owner_id = $7,
379 updated_at = $8
380 WHERE id = $1
381 "#,
382 )
383 .bind(unit.id)
384 .bind(&unit.unit_number)
385 .bind(unit_type_str)
386 .bind(unit.floor)
387 .bind(unit.surface_area)
388 .bind(unit.quota)
389 .bind(unit.owner_id)
390 .bind(unit.updated_at)
391 .execute(&self.pool)
392 .await
393 .map_err(|e| format!("Database error: {}", e))?;
394
395 Ok(unit.clone())
396 }
397
398 async fn delete(&self, id: Uuid) -> Result<bool, String> {
399 let result = sqlx::query("DELETE FROM units WHERE id = $1")
400 .bind(id)
401 .execute(&self.pool)
402 .await
403 .map_err(|e| format!("Database error: {}", e))?;
404
405 Ok(result.rows_affected() > 0)
406 }
407}