206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
|
|
package repositories
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"attune-heart-therapy/internal/models"
|
||
|
|
|
||
|
|
"gorm.io/gorm"
|
||
|
|
)
|
||
|
|
|
||
|
|
// scheduleRepository implements the ScheduleRepository interface
|
||
|
|
type scheduleRepository struct {
|
||
|
|
db *gorm.DB
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewScheduleRepository creates a new instance of ScheduleRepository
|
||
|
|
func NewScheduleRepository(db *gorm.DB) ScheduleRepository {
|
||
|
|
return &scheduleRepository{
|
||
|
|
db: db,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create creates a new schedule slot in the database
|
||
|
|
func (r *scheduleRepository) Create(schedule *models.Schedule) error {
|
||
|
|
if schedule == nil {
|
||
|
|
return errors.New("schedule cannot be nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for overlapping schedules
|
||
|
|
var count int64
|
||
|
|
if err := r.db.Model(&models.Schedule{}).
|
||
|
|
Where("((start_time <= ? AND end_time > ?) OR (start_time < ? AND end_time >= ?)) AND is_available = ?",
|
||
|
|
schedule.StartTime, schedule.StartTime, schedule.EndTime, schedule.EndTime, true).
|
||
|
|
Count(&count).Error; err != nil {
|
||
|
|
return fmt.Errorf("failed to check for overlapping schedules: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if count > 0 {
|
||
|
|
return errors.New("schedule slot overlaps with existing available slot")
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := r.db.Create(schedule).Error; err != nil {
|
||
|
|
return fmt.Errorf("failed to create schedule: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetAvailable retrieves all available schedule slots for a specific date
|
||
|
|
func (r *scheduleRepository) GetAvailable(date time.Time) ([]models.Schedule, error) {
|
||
|
|
var schedules []models.Schedule
|
||
|
|
|
||
|
|
// Get the start and end of the day
|
||
|
|
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
|
||
|
|
endOfDay := startOfDay.Add(24 * time.Hour)
|
||
|
|
|
||
|
|
if err := r.db.Where("is_available = ? AND start_time >= ? AND start_time < ? AND booked_count < max_bookings",
|
||
|
|
true, startOfDay, endOfDay).
|
||
|
|
Order("start_time ASC").
|
||
|
|
Find(&schedules).Error; err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to get available schedules for date %s: %w", date.Format("2006-01-02"), err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Filter out slots that are in the past
|
||
|
|
now := time.Now()
|
||
|
|
var availableSchedules []models.Schedule
|
||
|
|
for _, schedule := range schedules {
|
||
|
|
if schedule.StartTime.After(now) && schedule.IsAvailableForBooking() {
|
||
|
|
availableSchedules = append(availableSchedules, schedule)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return availableSchedules, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update updates an existing schedule slot in the database
|
||
|
|
func (r *scheduleRepository) Update(schedule *models.Schedule) error {
|
||
|
|
if schedule == nil {
|
||
|
|
return errors.New("schedule cannot be nil")
|
||
|
|
}
|
||
|
|
|
||
|
|
if schedule.ID == 0 {
|
||
|
|
return errors.New("schedule ID is required for update")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if schedule exists
|
||
|
|
var existingSchedule models.Schedule
|
||
|
|
if err := r.db.First(&existingSchedule, schedule.ID).Error; err != nil {
|
||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
|
|
return fmt.Errorf("schedule with ID %d not found", schedule.ID)
|
||
|
|
}
|
||
|
|
return fmt.Errorf("failed to check schedule existence: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// If updating time slots, check for overlaps (excluding current schedule)
|
||
|
|
if schedule.StartTime != existingSchedule.StartTime || schedule.EndTime != existingSchedule.EndTime {
|
||
|
|
var count int64
|
||
|
|
if err := r.db.Model(&models.Schedule{}).
|
||
|
|
Where("id != ? AND ((start_time <= ? AND end_time > ?) OR (start_time < ? AND end_time >= ?)) AND is_available = ?",
|
||
|
|
schedule.ID, schedule.StartTime, schedule.StartTime, schedule.EndTime, schedule.EndTime, true).
|
||
|
|
Count(&count).Error; err != nil {
|
||
|
|
return fmt.Errorf("failed to check for overlapping schedules: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if count > 0 {
|
||
|
|
return errors.New("updated schedule slot would overlap with existing available slot")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update the schedule
|
||
|
|
if err := r.db.Save(schedule).Error; err != nil {
|
||
|
|
return fmt.Errorf("failed to update schedule: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetByID retrieves a schedule slot by its ID
|
||
|
|
func (r *scheduleRepository) GetByID(id uint) (*models.Schedule, error) {
|
||
|
|
if id == 0 {
|
||
|
|
return nil, errors.New("invalid schedule ID")
|
||
|
|
}
|
||
|
|
|
||
|
|
var schedule models.Schedule
|
||
|
|
if err := r.db.First(&schedule, id).Error; err != nil {
|
||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
|
|
return nil, fmt.Errorf("schedule with ID %d not found", id)
|
||
|
|
}
|
||
|
|
return nil, fmt.Errorf("failed to get schedule by ID: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &schedule, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// IncrementBookedCount atomically increments the booked count for a schedule slot
|
||
|
|
// This method handles concurrent booking scenarios
|
||
|
|
func (r *scheduleRepository) IncrementBookedCount(scheduleID uint) error {
|
||
|
|
if scheduleID == 0 {
|
||
|
|
return errors.New("invalid schedule ID")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use a transaction to ensure atomicity
|
||
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
|
|
var schedule models.Schedule
|
||
|
|
|
||
|
|
// Lock the row for update to prevent race conditions
|
||
|
|
if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&schedule, scheduleID).Error; err != nil {
|
||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
|
|
return fmt.Errorf("schedule with ID %d not found", scheduleID)
|
||
|
|
}
|
||
|
|
return fmt.Errorf("failed to get schedule for update: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if slot is still available
|
||
|
|
if !schedule.IsAvailableForBooking() {
|
||
|
|
return errors.New("schedule slot is no longer available for booking")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Increment booked count
|
||
|
|
schedule.BookedCount++
|
||
|
|
|
||
|
|
if err := tx.Save(&schedule).Error; err != nil {
|
||
|
|
return fmt.Errorf("failed to increment booked count: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// DecrementBookedCount atomically decrements the booked count for a schedule slot
|
||
|
|
// This method is used when a booking is cancelled
|
||
|
|
func (r *scheduleRepository) DecrementBookedCount(scheduleID uint) error {
|
||
|
|
if scheduleID == 0 {
|
||
|
|
return errors.New("invalid schedule ID")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use a transaction to ensure atomicity
|
||
|
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
|
|
var schedule models.Schedule
|
||
|
|
|
||
|
|
// Lock the row for update to prevent race conditions
|
||
|
|
if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&schedule, scheduleID).Error; err != nil {
|
||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
|
|
return fmt.Errorf("schedule with ID %d not found", scheduleID)
|
||
|
|
}
|
||
|
|
return fmt.Errorf("failed to get schedule for update: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if booked count can be decremented
|
||
|
|
if schedule.BookedCount <= 0 {
|
||
|
|
return errors.New("booked count is already zero")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decrement booked count
|
||
|
|
schedule.BookedCount--
|
||
|
|
|
||
|
|
if err := tx.Save(&schedule).Error; err != nil {
|
||
|
|
return fmt.Errorf("failed to decrement booked count: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
}
|