1use crate::application::ports::SharedObjectRepository;
2use crate::domain::entities::{ObjectCondition, SharedObject, SharedObjectCategory};
3use crate::infrastructure::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresSharedObjectRepository {
9 pool: DbPool,
10}
11
12impl PostgresSharedObjectRepository {
13 pub fn new(pool: DbPool) -> Self {
14 Self { pool }
15 }
16}
17
18fn map_row_to_shared_object(row: &sqlx::postgres::PgRow) -> SharedObject {
20 let category_str: String = row.get("object_category");
21 let condition_str: String = row.get("condition");
22
23 SharedObject {
24 id: row.get("id"),
25 owner_id: row.get("owner_id"),
26 building_id: row.get("building_id"),
27 object_category: serde_json::from_str(&format!("\"{}\"", category_str))
28 .unwrap_or(SharedObjectCategory::Other),
29 object_name: row.get("object_name"),
30 description: row.get("description"),
31 condition: serde_json::from_str(&format!("\"{}\"", condition_str))
32 .unwrap_or(ObjectCondition::Good),
33 is_available: row.get("is_available"),
34 rental_credits_per_day: row.get("rental_credits_per_day"),
35 deposit_credits: row.get("deposit_credits"),
36 borrowing_duration_days: row.get("borrowing_duration_days"),
37 current_borrower_id: row.get("current_borrower_id"),
38 borrowed_at: row.get("borrowed_at"),
39 due_back_at: row.get("due_back_at"),
40 photos: row.get("photos"),
41 location_details: row.get("location_details"),
42 usage_instructions: row.get("usage_instructions"),
43 created_at: row.get("created_at"),
44 updated_at: row.get("updated_at"),
45 }
46}
47
48#[async_trait]
49impl SharedObjectRepository for PostgresSharedObjectRepository {
50 async fn create(&self, object: &SharedObject) -> Result<SharedObject, String> {
51 let category_str = serde_json::to_string(&object.object_category)
52 .map_err(|e| format!("Failed to serialize object_category: {}", e))?
53 .trim_matches('"')
54 .to_string();
55
56 let condition_str = serde_json::to_string(&object.condition)
57 .map_err(|e| format!("Failed to serialize condition: {}", e))?
58 .trim_matches('"')
59 .to_string();
60
61 sqlx::query(
62 r#"
63 INSERT INTO shared_objects (
64 id, owner_id, building_id, object_category, object_name, description,
65 condition, is_available, rental_credits_per_day, deposit_credits,
66 borrowing_duration_days, current_borrower_id, borrowed_at, due_back_at,
67 photos, location_details, usage_instructions, created_at, updated_at
68 )
69 VALUES ($1, $2, $3, $4::shared_object_category, $5, $6, $7::object_condition,
70 $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
71 "#,
72 )
73 .bind(object.id)
74 .bind(object.owner_id)
75 .bind(object.building_id)
76 .bind(&category_str)
77 .bind(&object.object_name)
78 .bind(&object.description)
79 .bind(&condition_str)
80 .bind(object.is_available)
81 .bind(object.rental_credits_per_day)
82 .bind(object.deposit_credits)
83 .bind(object.borrowing_duration_days)
84 .bind(object.current_borrower_id)
85 .bind(object.borrowed_at)
86 .bind(object.due_back_at)
87 .bind(&object.photos)
88 .bind(&object.location_details)
89 .bind(&object.usage_instructions)
90 .bind(object.created_at)
91 .bind(object.updated_at)
92 .execute(&self.pool)
93 .await
94 .map_err(|e| format!("Failed to create shared object: {}", e))?;
95
96 Ok(object.clone())
97 }
98
99 async fn find_by_id(&self, id: Uuid) -> Result<Option<SharedObject>, String> {
100 let row = sqlx::query(
101 r#"
102 SELECT id, owner_id, building_id, object_category::text AS object_category,
103 object_name, description, condition::text AS condition, is_available,
104 rental_credits_per_day, deposit_credits, borrowing_duration_days,
105 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
106 usage_instructions, created_at, updated_at
107 FROM shared_objects
108 WHERE id = $1
109 "#,
110 )
111 .bind(id)
112 .fetch_optional(&self.pool)
113 .await
114 .map_err(|e| format!("Failed to find shared object by ID: {}", e))?;
115
116 Ok(row.as_ref().map(map_row_to_shared_object))
117 }
118
119 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<SharedObject>, String> {
120 let rows = sqlx::query(
121 r#"
122 SELECT id, owner_id, building_id, object_category::text AS object_category,
123 object_name, description, condition::text AS condition, is_available,
124 rental_credits_per_day, deposit_credits, borrowing_duration_days,
125 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
126 usage_instructions, created_at, updated_at
127 FROM shared_objects
128 WHERE building_id = $1
129 ORDER BY
130 is_available DESC,
131 object_name ASC
132 "#,
133 )
134 .bind(building_id)
135 .fetch_all(&self.pool)
136 .await
137 .map_err(|e| format!("Failed to find shared objects by building: {}", e))?;
138
139 Ok(rows.iter().map(map_row_to_shared_object).collect())
140 }
141
142 async fn find_available_by_building(
143 &self,
144 building_id: Uuid,
145 ) -> Result<Vec<SharedObject>, String> {
146 let rows = sqlx::query(
147 r#"
148 SELECT id, owner_id, building_id, object_category::text AS object_category,
149 object_name, description, condition::text AS condition, is_available,
150 rental_credits_per_day, deposit_credits, borrowing_duration_days,
151 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
152 usage_instructions, created_at, updated_at
153 FROM shared_objects
154 WHERE building_id = $1 AND is_available = TRUE
155 ORDER BY object_name ASC
156 "#,
157 )
158 .bind(building_id)
159 .fetch_all(&self.pool)
160 .await
161 .map_err(|e| format!("Failed to find available shared objects by building: {}", e))?;
162
163 Ok(rows.iter().map(map_row_to_shared_object).collect())
164 }
165
166 async fn find_borrowed_by_building(
167 &self,
168 building_id: Uuid,
169 ) -> Result<Vec<SharedObject>, String> {
170 let rows = sqlx::query(
171 r#"
172 SELECT id, owner_id, building_id, object_category::text AS object_category,
173 object_name, description, condition::text AS condition, is_available,
174 rental_credits_per_day, deposit_credits, borrowing_duration_days,
175 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
176 usage_instructions, created_at, updated_at
177 FROM shared_objects
178 WHERE building_id = $1 AND current_borrower_id IS NOT NULL
179 ORDER BY due_back_at ASC
180 "#,
181 )
182 .bind(building_id)
183 .fetch_all(&self.pool)
184 .await
185 .map_err(|e| format!("Failed to find borrowed shared objects by building: {}", e))?;
186
187 Ok(rows.iter().map(map_row_to_shared_object).collect())
188 }
189
190 async fn find_overdue_by_building(
191 &self,
192 building_id: Uuid,
193 ) -> Result<Vec<SharedObject>, String> {
194 let rows = sqlx::query(
195 r#"
196 SELECT id, owner_id, building_id, object_category::text AS object_category,
197 object_name, description, condition::text AS condition, is_available,
198 rental_credits_per_day, deposit_credits, borrowing_duration_days,
199 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
200 usage_instructions, created_at, updated_at
201 FROM shared_objects
202 WHERE building_id = $1
203 AND current_borrower_id IS NOT NULL
204 AND due_back_at < NOW()
205 ORDER BY due_back_at ASC
206 "#,
207 )
208 .bind(building_id)
209 .fetch_all(&self.pool)
210 .await
211 .map_err(|e| format!("Failed to find overdue shared objects by building: {}", e))?;
212
213 Ok(rows.iter().map(map_row_to_shared_object).collect())
214 }
215
216 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<SharedObject>, String> {
217 let rows = sqlx::query(
218 r#"
219 SELECT id, owner_id, building_id, object_category::text AS object_category,
220 object_name, description, condition::text AS condition, is_available,
221 rental_credits_per_day, deposit_credits, borrowing_duration_days,
222 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
223 usage_instructions, created_at, updated_at
224 FROM shared_objects
225 WHERE owner_id = $1
226 ORDER BY created_at DESC
227 "#,
228 )
229 .bind(owner_id)
230 .fetch_all(&self.pool)
231 .await
232 .map_err(|e| format!("Failed to find shared objects by owner: {}", e))?;
233
234 Ok(rows.iter().map(map_row_to_shared_object).collect())
235 }
236
237 async fn find_borrowed_by_user(&self, borrower_id: Uuid) -> Result<Vec<SharedObject>, String> {
238 let rows = sqlx::query(
239 r#"
240 SELECT id, owner_id, building_id, object_category::text AS object_category,
241 object_name, description, condition::text AS condition, is_available,
242 rental_credits_per_day, deposit_credits, borrowing_duration_days,
243 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
244 usage_instructions, created_at, updated_at
245 FROM shared_objects
246 WHERE current_borrower_id = $1
247 ORDER BY due_back_at ASC
248 "#,
249 )
250 .bind(borrower_id)
251 .fetch_all(&self.pool)
252 .await
253 .map_err(|e| format!("Failed to find shared objects borrowed by user: {}", e))?;
254
255 Ok(rows.iter().map(map_row_to_shared_object).collect())
256 }
257
258 async fn find_by_category(
259 &self,
260 building_id: Uuid,
261 category: SharedObjectCategory,
262 ) -> Result<Vec<SharedObject>, String> {
263 let category_str = serde_json::to_string(&category)
264 .map_err(|e| format!("Failed to serialize category: {}", e))?
265 .trim_matches('"')
266 .to_string();
267
268 let rows = sqlx::query(
269 r#"
270 SELECT id, owner_id, building_id, object_category::text AS object_category,
271 object_name, description, condition::text AS condition, is_available,
272 rental_credits_per_day, deposit_credits, borrowing_duration_days,
273 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
274 usage_instructions, created_at, updated_at
275 FROM shared_objects
276 WHERE building_id = $1 AND object_category = $2::shared_object_category
277 ORDER BY
278 is_available DESC,
279 object_name ASC
280 "#,
281 )
282 .bind(building_id)
283 .bind(&category_str)
284 .fetch_all(&self.pool)
285 .await
286 .map_err(|e| format!("Failed to find shared objects by category: {}", e))?;
287
288 Ok(rows.iter().map(map_row_to_shared_object).collect())
289 }
290
291 async fn find_free_by_building(&self, building_id: Uuid) -> Result<Vec<SharedObject>, String> {
292 let rows = sqlx::query(
293 r#"
294 SELECT id, owner_id, building_id, object_category::text AS object_category,
295 object_name, description, condition::text AS condition, is_available,
296 rental_credits_per_day, deposit_credits, borrowing_duration_days,
297 current_borrower_id, borrowed_at, due_back_at, photos, location_details,
298 usage_instructions, created_at, updated_at
299 FROM shared_objects
300 WHERE building_id = $1
301 AND (rental_credits_per_day IS NULL OR rental_credits_per_day = 0)
302 ORDER BY
303 is_available DESC,
304 object_name ASC
305 "#,
306 )
307 .bind(building_id)
308 .fetch_all(&self.pool)
309 .await
310 .map_err(|e| format!("Failed to find free shared objects by building: {}", e))?;
311
312 Ok(rows.iter().map(map_row_to_shared_object).collect())
313 }
314
315 async fn update(&self, object: &SharedObject) -> Result<SharedObject, String> {
316 let category_str = serde_json::to_string(&object.object_category)
317 .map_err(|e| format!("Failed to serialize object_category: {}", e))?
318 .trim_matches('"')
319 .to_string();
320
321 let condition_str = serde_json::to_string(&object.condition)
322 .map_err(|e| format!("Failed to serialize condition: {}", e))?
323 .trim_matches('"')
324 .to_string();
325
326 sqlx::query(
327 r#"
328 UPDATE shared_objects
329 SET object_category = $2::shared_object_category,
330 object_name = $3,
331 description = $4,
332 condition = $5::object_condition,
333 is_available = $6,
334 rental_credits_per_day = $7,
335 deposit_credits = $8,
336 borrowing_duration_days = $9,
337 current_borrower_id = $10,
338 borrowed_at = $11,
339 due_back_at = $12,
340 photos = $13,
341 location_details = $14,
342 usage_instructions = $15,
343 updated_at = $16
344 WHERE id = $1
345 "#,
346 )
347 .bind(object.id)
348 .bind(&category_str)
349 .bind(&object.object_name)
350 .bind(&object.description)
351 .bind(&condition_str)
352 .bind(object.is_available)
353 .bind(object.rental_credits_per_day)
354 .bind(object.deposit_credits)
355 .bind(object.borrowing_duration_days)
356 .bind(object.current_borrower_id)
357 .bind(object.borrowed_at)
358 .bind(object.due_back_at)
359 .bind(&object.photos)
360 .bind(&object.location_details)
361 .bind(&object.usage_instructions)
362 .bind(object.updated_at)
363 .execute(&self.pool)
364 .await
365 .map_err(|e| format!("Failed to update shared object: {}", e))?;
366
367 Ok(object.clone())
368 }
369
370 async fn delete(&self, id: Uuid) -> Result<(), String> {
371 sqlx::query(
372 r#"
373 DELETE FROM shared_objects WHERE id = $1
374 "#,
375 )
376 .bind(id)
377 .execute(&self.pool)
378 .await
379 .map_err(|e| format!("Failed to delete shared object: {}", e))?;
380
381 Ok(())
382 }
383
384 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
385 let row = sqlx::query(
386 r#"
387 SELECT COUNT(*) as count FROM shared_objects WHERE building_id = $1
388 "#,
389 )
390 .bind(building_id)
391 .fetch_one(&self.pool)
392 .await
393 .map_err(|e| format!("Failed to count shared objects by building: {}", e))?;
394
395 Ok(row.get("count"))
396 }
397
398 async fn count_available_by_building(&self, building_id: Uuid) -> Result<i64, String> {
399 let row = sqlx::query(
400 r#"
401 SELECT COUNT(*) as count
402 FROM shared_objects
403 WHERE building_id = $1 AND is_available = TRUE
404 "#,
405 )
406 .bind(building_id)
407 .fetch_one(&self.pool)
408 .await
409 .map_err(|e| {
410 format!(
411 "Failed to count available shared objects by building: {}",
412 e
413 )
414 })?;
415
416 Ok(row.get("count"))
417 }
418
419 async fn count_borrowed_by_building(&self, building_id: Uuid) -> Result<i64, String> {
420 let row = sqlx::query(
421 r#"
422 SELECT COUNT(*) as count
423 FROM shared_objects
424 WHERE building_id = $1 AND current_borrower_id IS NOT NULL
425 "#,
426 )
427 .bind(building_id)
428 .fetch_one(&self.pool)
429 .await
430 .map_err(|e| format!("Failed to count borrowed shared objects by building: {}", e))?;
431
432 Ok(row.get("count"))
433 }
434
435 async fn count_overdue_by_building(&self, building_id: Uuid) -> Result<i64, String> {
436 let row = sqlx::query(
437 r#"
438 SELECT COUNT(*) as count
439 FROM shared_objects
440 WHERE building_id = $1
441 AND current_borrower_id IS NOT NULL
442 AND due_back_at < NOW()
443 "#,
444 )
445 .bind(building_id)
446 .fetch_one(&self.pool)
447 .await
448 .map_err(|e| format!("Failed to count overdue shared objects by building: {}", e))?;
449
450 Ok(row.get("count"))
451 }
452
453 async fn count_by_category(
454 &self,
455 building_id: Uuid,
456 category: SharedObjectCategory,
457 ) -> Result<i64, String> {
458 let category_str = serde_json::to_string(&category)
459 .map_err(|e| format!("Failed to serialize category: {}", e))?
460 .trim_matches('"')
461 .to_string();
462
463 let row = sqlx::query(
464 r#"
465 SELECT COUNT(*) as count
466 FROM shared_objects
467 WHERE building_id = $1 AND object_category = $2::shared_object_category
468 "#,
469 )
470 .bind(building_id)
471 .bind(&category_str)
472 .fetch_one(&self.pool)
473 .await
474 .map_err(|e| format!("Failed to count shared objects by category: {}", e))?;
475
476 Ok(row.get("count"))
477 }
478}