1use crate::application::dto::BookingStatisticsDto;
2use crate::application::ports::ResourceBookingRepository;
3use crate::domain::entities::{BookingStatus, RecurringPattern, ResourceBooking, ResourceType};
4use crate::infrastructure::pool::DbPool;
5use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use sqlx::Row;
8use uuid::Uuid;
9
10pub struct PostgresResourceBookingRepository {
11 pool: DbPool,
12}
13
14impl PostgresResourceBookingRepository {
15 pub fn new(pool: DbPool) -> Self {
16 Self { pool }
17 }
18
19 fn row_to_entity(row: &sqlx::postgres::PgRow) -> Result<ResourceBooking, String> {
21 let resource_type_str: String = row
23 .try_get("resource_type")
24 .map_err(|e| format!("Failed to get resource_type: {}", e))?;
25 let resource_type: ResourceType =
26 serde_json::from_str(&format!("\"{}\"", resource_type_str))
27 .map_err(|e| format!("Failed to parse resource_type: {}", e))?;
28
29 let status_str: String = row
30 .try_get("status")
31 .map_err(|e| format!("Failed to get status: {}", e))?;
32 let status: BookingStatus = serde_json::from_str(&format!("\"{}\"", status_str))
33 .map_err(|e| format!("Failed to parse status: {}", e))?;
34
35 let recurring_pattern_str: String = row
36 .try_get("recurring_pattern")
37 .map_err(|e| format!("Failed to get recurring_pattern: {}", e))?;
38 let recurring_pattern: RecurringPattern =
39 serde_json::from_str(&format!("\"{}\"", recurring_pattern_str))
40 .map_err(|e| format!("Failed to parse recurring_pattern: {}", e))?;
41
42 Ok(ResourceBooking {
43 id: row
44 .try_get("id")
45 .map_err(|e| format!("Failed to get id: {}", e))?,
46 building_id: row
47 .try_get("building_id")
48 .map_err(|e| format!("Failed to get building_id: {}", e))?,
49 resource_type,
50 resource_name: row
51 .try_get("resource_name")
52 .map_err(|e| format!("Failed to get resource_name: {}", e))?,
53 booked_by: row
54 .try_get("booked_by")
55 .map_err(|e| format!("Failed to get booked_by: {}", e))?,
56 start_time: row
57 .try_get("start_time")
58 .map_err(|e| format!("Failed to get start_time: {}", e))?,
59 end_time: row
60 .try_get("end_time")
61 .map_err(|e| format!("Failed to get end_time: {}", e))?,
62 status,
63 notes: row
64 .try_get("notes")
65 .map_err(|e| format!("Failed to get notes: {}", e))?,
66 recurring_pattern,
67 recurrence_end_date: row
68 .try_get("recurrence_end_date")
69 .map_err(|e| format!("Failed to get recurrence_end_date: {}", e))?,
70 created_at: row
71 .try_get("created_at")
72 .map_err(|e| format!("Failed to get created_at: {}", e))?,
73 updated_at: row
74 .try_get("updated_at")
75 .map_err(|e| format!("Failed to get updated_at: {}", e))?,
76 })
77 }
78}
79
80#[async_trait]
81impl ResourceBookingRepository for PostgresResourceBookingRepository {
82 async fn create(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
83 let resource_type_str = serde_json::to_string(&booking.resource_type)
85 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
86 .trim_matches('"')
87 .to_string();
88
89 let status_str = serde_json::to_string(&booking.status)
90 .map_err(|e| format!("Failed to serialize status: {}", e))?
91 .trim_matches('"')
92 .to_string();
93
94 let recurring_pattern_str = serde_json::to_string(&booking.recurring_pattern)
95 .map_err(|e| format!("Failed to serialize recurring_pattern: {}", e))?
96 .trim_matches('"')
97 .to_string();
98
99 sqlx::query(
100 r#"
101 INSERT INTO resource_bookings (
102 id, building_id, resource_type, resource_name, booked_by,
103 start_time, end_time, status, notes, recurring_pattern,
104 recurrence_end_date, created_at, updated_at
105 )
106 VALUES ($1, $2, $3::resource_type, $4, $5, $6, $7, $8::booking_status, $9,
107 $10::recurring_pattern, $11, $12, $13)
108 "#,
109 )
110 .bind(booking.id)
111 .bind(booking.building_id)
112 .bind(&resource_type_str)
113 .bind(&booking.resource_name)
114 .bind(booking.booked_by)
115 .bind(booking.start_time)
116 .bind(booking.end_time)
117 .bind(&status_str)
118 .bind(&booking.notes)
119 .bind(&recurring_pattern_str)
120 .bind(booking.recurrence_end_date)
121 .bind(booking.created_at)
122 .bind(booking.updated_at)
123 .execute(&self.pool)
124 .await
125 .map_err(|e| format!("Failed to create resource booking: {}", e))?;
126
127 Ok(booking.clone())
128 }
129
130 async fn find_by_id(&self, id: Uuid) -> Result<Option<ResourceBooking>, String> {
131 let row = sqlx::query(
132 r#"
133 SELECT id, building_id, resource_type, resource_name, booked_by,
134 start_time, end_time, status, notes, recurring_pattern,
135 recurrence_end_date, created_at, updated_at
136 FROM resource_bookings
137 WHERE id = $1
138 "#,
139 )
140 .bind(id)
141 .fetch_optional(&self.pool)
142 .await
143 .map_err(|e| format!("Failed to find resource booking: {}", e))?;
144
145 match row {
146 Some(r) => Ok(Some(Self::row_to_entity(&r)?)),
147 None => Ok(None),
148 }
149 }
150
151 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
152 let rows = sqlx::query(
153 r#"
154 SELECT id, building_id, resource_type, resource_name, booked_by,
155 start_time, end_time, status, notes, recurring_pattern,
156 recurrence_end_date, created_at, updated_at
157 FROM resource_bookings
158 WHERE building_id = $1
159 ORDER BY start_time ASC
160 "#,
161 )
162 .bind(building_id)
163 .fetch_all(&self.pool)
164 .await
165 .map_err(|e| format!("Failed to find bookings by building: {}", e))?;
166
167 rows.iter().map(Self::row_to_entity).collect()
168 }
169
170 async fn find_by_building_and_resource_type(
171 &self,
172 building_id: Uuid,
173 resource_type: ResourceType,
174 ) -> Result<Vec<ResourceBooking>, String> {
175 let resource_type_str = serde_json::to_string(&resource_type)
176 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
177 .trim_matches('"')
178 .to_string();
179
180 let rows = sqlx::query(
181 r#"
182 SELECT id, building_id, resource_type, resource_name, booked_by,
183 start_time, end_time, status, notes, recurring_pattern,
184 recurrence_end_date, created_at, updated_at
185 FROM resource_bookings
186 WHERE building_id = $1 AND resource_type = $2::resource_type
187 ORDER BY start_time ASC
188 "#,
189 )
190 .bind(building_id)
191 .bind(&resource_type_str)
192 .fetch_all(&self.pool)
193 .await
194 .map_err(|e| {
195 format!(
196 "Failed to find bookings by building and resource type: {}",
197 e
198 )
199 })?;
200
201 rows.iter().map(Self::row_to_entity).collect()
202 }
203
204 async fn find_by_resource(
205 &self,
206 building_id: Uuid,
207 resource_type: ResourceType,
208 resource_name: &str,
209 ) -> Result<Vec<ResourceBooking>, String> {
210 let resource_type_str = serde_json::to_string(&resource_type)
211 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
212 .trim_matches('"')
213 .to_string();
214
215 let rows = sqlx::query(
216 r#"
217 SELECT id, building_id, resource_type, resource_name, booked_by,
218 start_time, end_time, status, notes, recurring_pattern,
219 recurrence_end_date, created_at, updated_at
220 FROM resource_bookings
221 WHERE building_id = $1 AND resource_type = $2::resource_type AND resource_name = $3
222 ORDER BY start_time ASC
223 "#,
224 )
225 .bind(building_id)
226 .bind(&resource_type_str)
227 .bind(resource_name)
228 .fetch_all(&self.pool)
229 .await
230 .map_err(|e| format!("Failed to find bookings by resource: {}", e))?;
231
232 rows.iter().map(Self::row_to_entity).collect()
233 }
234
235 async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
236 let rows = sqlx::query(
237 r#"
238 SELECT id, building_id, resource_type, resource_name, booked_by,
239 start_time, end_time, status, notes, recurring_pattern,
240 recurrence_end_date, created_at, updated_at
241 FROM resource_bookings
242 WHERE booked_by = $1
243 ORDER BY start_time DESC
244 "#,
245 )
246 .bind(user_id)
247 .fetch_all(&self.pool)
248 .await
249 .map_err(|e| format!("Failed to find bookings by user: {}", e))?;
250
251 rows.iter().map(Self::row_to_entity).collect()
252 }
253
254 async fn find_by_user_and_status(
255 &self,
256 user_id: Uuid,
257 status: BookingStatus,
258 ) -> Result<Vec<ResourceBooking>, String> {
259 let status_str = serde_json::to_string(&status)
260 .map_err(|e| format!("Failed to serialize status: {}", e))?
261 .trim_matches('"')
262 .to_string();
263
264 let rows = sqlx::query(
265 r#"
266 SELECT id, building_id, resource_type, resource_name, booked_by,
267 start_time, end_time, status, notes, recurring_pattern,
268 recurrence_end_date, created_at, updated_at
269 FROM resource_bookings
270 WHERE booked_by = $1 AND status = $2::booking_status
271 ORDER BY start_time DESC
272 "#,
273 )
274 .bind(user_id)
275 .bind(&status_str)
276 .fetch_all(&self.pool)
277 .await
278 .map_err(|e| format!("Failed to find bookings by user and status: {}", e))?;
279
280 rows.iter().map(Self::row_to_entity).collect()
281 }
282
283 async fn find_by_building_and_status(
284 &self,
285 building_id: Uuid,
286 status: BookingStatus,
287 ) -> Result<Vec<ResourceBooking>, String> {
288 let status_str = serde_json::to_string(&status)
289 .map_err(|e| format!("Failed to serialize status: {}", e))?
290 .trim_matches('"')
291 .to_string();
292
293 let rows = sqlx::query(
294 r#"
295 SELECT id, building_id, resource_type, resource_name, booked_by,
296 start_time, end_time, status, notes, recurring_pattern,
297 recurrence_end_date, created_at, updated_at
298 FROM resource_bookings
299 WHERE building_id = $1 AND status = $2::booking_status
300 ORDER BY start_time ASC
301 "#,
302 )
303 .bind(building_id)
304 .bind(&status_str)
305 .fetch_all(&self.pool)
306 .await
307 .map_err(|e| format!("Failed to find bookings by building and status: {}", e))?;
308
309 rows.iter().map(Self::row_to_entity).collect()
310 }
311
312 async fn find_upcoming(
313 &self,
314 building_id: Uuid,
315 limit: Option<i64>,
316 ) -> Result<Vec<ResourceBooking>, String> {
317 let limit_val = limit.unwrap_or(50);
318
319 let rows = sqlx::query(
320 r#"
321 SELECT id, building_id, resource_type, resource_name, booked_by,
322 start_time, end_time, status, notes, recurring_pattern,
323 recurrence_end_date, created_at, updated_at
324 FROM resource_bookings
325 WHERE building_id = $1
326 AND start_time > NOW()
327 AND status IN ('Confirmed', 'Pending')
328 ORDER BY start_time ASC
329 LIMIT $2
330 "#,
331 )
332 .bind(building_id)
333 .bind(limit_val)
334 .fetch_all(&self.pool)
335 .await
336 .map_err(|e| format!("Failed to find upcoming bookings: {}", e))?;
337
338 rows.iter().map(Self::row_to_entity).collect()
339 }
340
341 async fn find_active(&self, building_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
342 let rows = sqlx::query(
343 r#"
344 SELECT id, building_id, resource_type, resource_name, booked_by,
345 start_time, end_time, status, notes, recurring_pattern,
346 recurrence_end_date, created_at, updated_at
347 FROM resource_bookings
348 WHERE building_id = $1
349 AND status = 'Confirmed'
350 AND start_time <= NOW()
351 AND end_time > NOW()
352 ORDER BY start_time ASC
353 "#,
354 )
355 .bind(building_id)
356 .fetch_all(&self.pool)
357 .await
358 .map_err(|e| format!("Failed to find active bookings: {}", e))?;
359
360 rows.iter().map(Self::row_to_entity).collect()
361 }
362
363 async fn find_past(
364 &self,
365 building_id: Uuid,
366 limit: Option<i64>,
367 ) -> Result<Vec<ResourceBooking>, String> {
368 let limit_val = limit.unwrap_or(50);
369
370 let rows = sqlx::query(
371 r#"
372 SELECT id, building_id, resource_type, resource_name, booked_by,
373 start_time, end_time, status, notes, recurring_pattern,
374 recurrence_end_date, created_at, updated_at
375 FROM resource_bookings
376 WHERE building_id = $1
377 AND end_time < NOW()
378 ORDER BY start_time DESC
379 LIMIT $2
380 "#,
381 )
382 .bind(building_id)
383 .bind(limit_val)
384 .fetch_all(&self.pool)
385 .await
386 .map_err(|e| format!("Failed to find past bookings: {}", e))?;
387
388 rows.iter().map(Self::row_to_entity).collect()
389 }
390
391 async fn find_conflicts(
392 &self,
393 building_id: Uuid,
394 resource_type: ResourceType,
395 resource_name: &str,
396 start_time: DateTime<Utc>,
397 end_time: DateTime<Utc>,
398 exclude_booking_id: Option<Uuid>,
399 ) -> Result<Vec<ResourceBooking>, String> {
400 let resource_type_str = serde_json::to_string(&resource_type)
401 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
402 .trim_matches('"')
403 .to_string();
404
405 let rows = if let Some(exclude_id) = exclude_booking_id {
408 sqlx::query(
409 r#"
410 SELECT id, building_id, resource_type, resource_name, booked_by,
411 start_time, end_time, status, notes, recurring_pattern,
412 recurrence_end_date, created_at, updated_at
413 FROM resource_bookings
414 WHERE building_id = $1
415 AND resource_type = $2::resource_type
416 AND resource_name = $3
417 AND status IN ('Pending', 'Confirmed')
418 AND start_time < $5
419 AND end_time > $4
420 AND id != $6
421 ORDER BY start_time ASC
422 "#,
423 )
424 .bind(building_id)
425 .bind(&resource_type_str)
426 .bind(resource_name)
427 .bind(start_time)
428 .bind(end_time)
429 .bind(exclude_id)
430 .fetch_all(&self.pool)
431 .await
432 } else {
433 sqlx::query(
434 r#"
435 SELECT id, building_id, resource_type, resource_name, booked_by,
436 start_time, end_time, status, notes, recurring_pattern,
437 recurrence_end_date, created_at, updated_at
438 FROM resource_bookings
439 WHERE building_id = $1
440 AND resource_type = $2::resource_type
441 AND resource_name = $3
442 AND status IN ('Pending', 'Confirmed')
443 AND start_time < $5
444 AND end_time > $4
445 ORDER BY start_time ASC
446 "#,
447 )
448 .bind(building_id)
449 .bind(&resource_type_str)
450 .bind(resource_name)
451 .bind(start_time)
452 .bind(end_time)
453 .fetch_all(&self.pool)
454 .await
455 }
456 .map_err(|e| format!("Failed to find conflicting bookings: {}", e))?;
457
458 rows.iter().map(Self::row_to_entity).collect()
459 }
460
461 async fn update(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
462 let resource_type_str = serde_json::to_string(&booking.resource_type)
463 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
464 .trim_matches('"')
465 .to_string();
466
467 let status_str = serde_json::to_string(&booking.status)
468 .map_err(|e| format!("Failed to serialize status: {}", e))?
469 .trim_matches('"')
470 .to_string();
471
472 let recurring_pattern_str = serde_json::to_string(&booking.recurring_pattern)
473 .map_err(|e| format!("Failed to serialize recurring_pattern: {}", e))?
474 .trim_matches('"')
475 .to_string();
476
477 let result = sqlx::query(
478 r#"
479 UPDATE resource_bookings
480 SET resource_type = $2::resource_type,
481 resource_name = $3,
482 start_time = $4,
483 end_time = $5,
484 status = $6::booking_status,
485 notes = $7,
486 recurring_pattern = $8::recurring_pattern,
487 recurrence_end_date = $9,
488 updated_at = $10
489 WHERE id = $1
490 "#,
491 )
492 .bind(booking.id)
493 .bind(&resource_type_str)
494 .bind(&booking.resource_name)
495 .bind(booking.start_time)
496 .bind(booking.end_time)
497 .bind(&status_str)
498 .bind(&booking.notes)
499 .bind(&recurring_pattern_str)
500 .bind(booking.recurrence_end_date)
501 .bind(booking.updated_at)
502 .execute(&self.pool)
503 .await
504 .map_err(|e| format!("Failed to update resource booking: {}", e))?;
505
506 if result.rows_affected() == 0 {
507 return Err("Resource booking not found".to_string());
508 }
509
510 Ok(booking.clone())
511 }
512
513 async fn delete(&self, id: Uuid) -> Result<(), String> {
514 let result = sqlx::query(
515 r#"
516 DELETE FROM resource_bookings
517 WHERE id = $1
518 "#,
519 )
520 .bind(id)
521 .execute(&self.pool)
522 .await
523 .map_err(|e| format!("Failed to delete resource booking: {}", e))?;
524
525 if result.rows_affected() == 0 {
526 return Err("Resource booking not found".to_string());
527 }
528
529 Ok(())
530 }
531
532 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
533 let row = sqlx::query(
534 r#"
535 SELECT COUNT(*) as count
536 FROM resource_bookings
537 WHERE building_id = $1
538 "#,
539 )
540 .bind(building_id)
541 .fetch_one(&self.pool)
542 .await
543 .map_err(|e| format!("Failed to count bookings by building: {}", e))?;
544
545 let count: i64 = row
546 .try_get("count")
547 .map_err(|e| format!("Failed to get count: {}", e))?;
548 Ok(count)
549 }
550
551 async fn count_by_building_and_status(
552 &self,
553 building_id: Uuid,
554 status: BookingStatus,
555 ) -> Result<i64, String> {
556 let status_str = serde_json::to_string(&status)
557 .map_err(|e| format!("Failed to serialize status: {}", e))?
558 .trim_matches('"')
559 .to_string();
560
561 let row = sqlx::query(
562 r#"
563 SELECT COUNT(*) as count
564 FROM resource_bookings
565 WHERE building_id = $1 AND status = $2::booking_status
566 "#,
567 )
568 .bind(building_id)
569 .bind(&status_str)
570 .fetch_one(&self.pool)
571 .await
572 .map_err(|e| format!("Failed to count bookings by building and status: {}", e))?;
573
574 let count: i64 = row
575 .try_get("count")
576 .map_err(|e| format!("Failed to get count: {}", e))?;
577 Ok(count)
578 }
579
580 async fn count_by_resource(
581 &self,
582 building_id: Uuid,
583 resource_type: ResourceType,
584 resource_name: &str,
585 ) -> Result<i64, String> {
586 let resource_type_str = serde_json::to_string(&resource_type)
587 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
588 .trim_matches('"')
589 .to_string();
590
591 let row = sqlx::query(
592 r#"
593 SELECT COUNT(*) as count
594 FROM resource_bookings
595 WHERE building_id = $1 AND resource_type = $2::resource_type AND resource_name = $3
596 "#,
597 )
598 .bind(building_id)
599 .bind(&resource_type_str)
600 .bind(resource_name)
601 .fetch_one(&self.pool)
602 .await
603 .map_err(|e| format!("Failed to count bookings by resource: {}", e))?;
604
605 let count: i64 = row
606 .try_get("count")
607 .map_err(|e| format!("Failed to get count: {}", e))?;
608 Ok(count)
609 }
610
611 async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
612 let total = self.count_by_building(building_id).await?;
614 let confirmed = self
615 .count_by_building_and_status(building_id, BookingStatus::Confirmed)
616 .await?;
617 let pending = self
618 .count_by_building_and_status(building_id, BookingStatus::Pending)
619 .await?;
620 let completed = self
621 .count_by_building_and_status(building_id, BookingStatus::Completed)
622 .await?;
623 let cancelled = self
624 .count_by_building_and_status(building_id, BookingStatus::Cancelled)
625 .await?;
626 let no_show = self
627 .count_by_building_and_status(building_id, BookingStatus::NoShow)
628 .await?;
629
630 let active_bookings = self.find_active(building_id).await?.len() as i64;
632
633 let upcoming_row = sqlx::query(
635 r#"
636 SELECT COUNT(*) as count
637 FROM resource_bookings
638 WHERE building_id = $1
639 AND start_time > NOW()
640 AND status IN ('Confirmed', 'Pending')
641 "#,
642 )
643 .bind(building_id)
644 .fetch_one(&self.pool)
645 .await
646 .map_err(|e| format!("Failed to count upcoming bookings: {}", e))?;
647
648 let upcoming_bookings: i64 = upcoming_row
649 .try_get("count")
650 .map_err(|e| format!("Failed to get upcoming count: {}", e))?;
651
652 let hours_row = sqlx::query(
654 r#"
655 SELECT COALESCE(SUM(EXTRACT(EPOCH FROM (end_time - start_time)) / 3600), 0) as total_hours
656 FROM resource_bookings
657 WHERE building_id = $1
658 AND status IN ('Confirmed', 'Completed')
659 "#,
660 )
661 .bind(building_id)
662 .fetch_one(&self.pool)
663 .await
664 .map_err(|e| format!("Failed to calculate total hours booked: {}", e))?;
665
666 let total_hours_booked: f64 = hours_row
667 .try_get("total_hours")
668 .map_err(|e| format!("Failed to get total_hours: {}", e))?;
669
670 let popular_row = sqlx::query(
672 r#"
673 SELECT resource_name, COUNT(*) as booking_count
674 FROM resource_bookings
675 WHERE building_id = $1
676 GROUP BY resource_name
677 ORDER BY booking_count DESC
678 LIMIT 1
679 "#,
680 )
681 .bind(building_id)
682 .fetch_optional(&self.pool)
683 .await
684 .map_err(|e| format!("Failed to find most popular resource: {}", e))?;
685
686 let most_popular_resource = popular_row.map(|row| {
687 row.try_get::<String, _>("resource_name")
688 .unwrap_or_default()
689 });
690
691 Ok(BookingStatisticsDto {
692 building_id,
693 total_bookings: total,
694 confirmed_bookings: confirmed,
695 pending_bookings: pending,
696 completed_bookings: completed,
697 cancelled_bookings: cancelled,
698 no_show_bookings: no_show,
699 active_bookings,
700 upcoming_bookings,
701 total_hours_booked,
702 most_popular_resource,
703 })
704 }
705}