bookings.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // Package repository
  2. package repository
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "math"
  8. "time"
  9. "github.com/jackc/pgx/v5"
  10. "github.com/jackc/pgx/v5/pgxpool"
  11. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models"
  12. )
  13. var ErrPlaceNotFound = errors.New("place not found")
  14. type BookingRepo struct {
  15. pool *pgxpool.Pool
  16. }
  17. func NewBookingRepo(pool *pgxpool.Pool) *BookingRepo {
  18. return &BookingRepo{pool: pool}
  19. }
  20. func (r *BookingRepo) Create(ctx context.Context, b *models.Booking) error {
  21. tx, err := r.pool.Begin(ctx)
  22. if err != nil {
  23. return fmt.Errorf("begin tx: %w", err)
  24. }
  25. defer tx.Rollback(ctx)
  26. var hourlyRate *int
  27. err = tx.QueryRow(ctx,
  28. `SELECT hourly_rate, currency FROM places WHERE id = $1 AND deleted_at IS NULL FOR UPDATE`,
  29. b.PlaceID).Scan(&hourlyRate, &b.Currency)
  30. if err != nil {
  31. if errors.Is(err, pgx.ErrNoRows) {
  32. return ErrPlaceNotFound
  33. }
  34. return fmt.Errorf("lock place: %w", err)
  35. }
  36. if hourlyRate != nil {
  37. hours := math.Ceil(b.EndTime.Sub(b.StartTime).Hours())
  38. if hours < 1 {
  39. hours = 1
  40. }
  41. total := *hourlyRate * int(hours)
  42. b.TotalPrice = &total
  43. }
  44. err = tx.QueryRow(ctx,
  45. `INSERT INTO bookings (place_id, user_id, start_time, end_time, status, total_price, currency, comment)
  46. VALUES ($1, $2, $3, $4, 'pending', $5, $6, $7)
  47. RETURNING id, created_at, updated_at`,
  48. b.PlaceID, b.UserID, b.StartTime, b.EndTime, b.TotalPrice, b.Currency, b.Comment,
  49. ).Scan(&b.ID, &b.CreatedAt, &b.UpdatedAt)
  50. if err != nil {
  51. return fmt.Errorf("insert booking: %w", err)
  52. }
  53. return tx.Commit(ctx)
  54. }
  55. func (r *BookingRepo) IsTimeSlotAvailable(ctx context.Context, placeID string, start, end time.Time) (bool, error) {
  56. var count int
  57. err := r.pool.QueryRow(ctx,
  58. `SELECT COUNT(*) FROM bookings
  59. WHERE place_id = $1 AND status != 'cancelled'
  60. AND tsrange(start_time, end_time) && tsrange($2, $3)`,
  61. placeID, start, end).Scan(&count)
  62. if err != nil {
  63. return false, err
  64. }
  65. return count == 0, nil
  66. }
  67. func (r *BookingRepo) GetByID(ctx context.Context, id string) (*models.Booking, error) {
  68. row := r.pool.QueryRow(ctx,
  69. `SELECT id, place_id, user_id, start_time, end_time, status, total_price, currency, comment,
  70. created_at, updated_at
  71. FROM bookings WHERE id = $1`, id)
  72. var b models.Booking
  73. err := row.Scan(&b.ID, &b.PlaceID, &b.UserID, &b.StartTime, &b.EndTime,
  74. &b.Status, &b.TotalPrice, &b.Currency, &b.Comment, &b.CreatedAt, &b.UpdatedAt)
  75. if err != nil {
  76. if err == pgx.ErrNoRows {
  77. return nil, nil
  78. }
  79. return nil, err
  80. }
  81. return &b, nil
  82. }
  83. func (r *BookingRepo) ListByUser(ctx context.Context, userID string) ([]*models.Booking, error) {
  84. rows, err := r.pool.Query(ctx,
  85. `SELECT id, place_id, user_id, start_time, end_time, status, total_price, currency, comment,
  86. created_at, updated_at
  87. FROM bookings WHERE user_id = $1 ORDER BY start_time DESC`, userID)
  88. if err != nil {
  89. return nil, err
  90. }
  91. defer rows.Close()
  92. return scanBookings(rows)
  93. }
  94. func (r *BookingRepo) ListByPlace(ctx context.Context, placeID string) ([]*models.Booking, error) {
  95. rows, err := r.pool.Query(ctx,
  96. `SELECT id, place_id, user_id, start_time, end_time, status, total_price, currency, comment,
  97. created_at, updated_at
  98. FROM bookings WHERE place_id = $1 ORDER BY start_time DESC`, placeID)
  99. if err != nil {
  100. return nil, err
  101. }
  102. defer rows.Close()
  103. return scanBookings(rows)
  104. }
  105. func (r *BookingRepo) UpdateStatus(ctx context.Context, id, status string) error {
  106. _, err := r.pool.Exec(ctx,
  107. `UPDATE bookings SET status=$1, updated_at=now() WHERE id=$2`, status, id)
  108. return err
  109. }
  110. func (r *BookingRepo) UpdateStatusIfPending(ctx context.Context, id, status string) (bool, error) {
  111. tag, err := r.pool.Exec(ctx,
  112. `UPDATE bookings SET status=$1, updated_at=now() WHERE id=$2 AND status='pending'`, status, id)
  113. if err != nil {
  114. return false, fmt.Errorf("update status if pending: %w", err)
  115. }
  116. return tag.RowsAffected() > 0, nil
  117. }
  118. func (r *BookingRepo) GetPlaceMinHours(ctx context.Context, placeID string) (int, error) {
  119. var minHours int
  120. err := r.pool.QueryRow(ctx,
  121. `SELECT min_hours FROM places WHERE id = $1 AND deleted_at IS NULL`, placeID).Scan(&minHours)
  122. if err != nil {
  123. if errors.Is(err, pgx.ErrNoRows) {
  124. return 0, ErrPlaceNotFound
  125. }
  126. return 0, fmt.Errorf("get place min_hours: %w", err)
  127. }
  128. return minHours, nil
  129. }
  130. func scanBookings(rows pgx.Rows) ([]*models.Booking, error) {
  131. var bookings []*models.Booking
  132. for rows.Next() {
  133. var b models.Booking
  134. if err := rows.Scan(&b.ID, &b.PlaceID, &b.UserID, &b.StartTime, &b.EndTime,
  135. &b.Status, &b.TotalPrice, &b.Currency, &b.Comment, &b.CreatedAt, &b.UpdatedAt); err != nil {
  136. return nil, err
  137. }
  138. bookings = append(bookings, &b)
  139. }
  140. return bookings, nil
  141. }