koprogo_api/infrastructure/database/repositories/
shared_object_repository_impl.rs

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
18/// Helper function to map database row to SharedObject entity
19fn 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}