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