backend-service/internal/repositories/schedule_repository.go

206 lines
6.2 KiB
Go
Raw Normal View History

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