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 }) }