package services import ( "fmt" "log" "time" "attune-heart-therapy/internal/models" "attune-heart-therapy/internal/repositories" ) // bookingService implements the BookingService interface type bookingService struct { bookingRepo repositories.BookingRepository scheduleRepo repositories.ScheduleRepository userRepo repositories.UserRepository jitsiService JitsiService paymentService PaymentService notificationService NotificationService } // NewBookingService creates a new instance of BookingService func NewBookingService( bookingRepo repositories.BookingRepository, scheduleRepo repositories.ScheduleRepository, userRepo repositories.UserRepository, jitsiService JitsiService, paymentService PaymentService, notificationService NotificationService, ) BookingService { return &bookingService{ bookingRepo: bookingRepo, scheduleRepo: scheduleRepo, userRepo: userRepo, jitsiService: jitsiService, paymentService: paymentService, notificationService: notificationService, } } // GetAvailableSlots retrieves available time slots for a given date func (s *bookingService) GetAvailableSlots(date time.Time) ([]models.Schedule, error) { slots, err := s.scheduleRepo.GetAvailable(date) if err != nil { log.Printf("Failed to get available slots for date %v: %v", date, err) return nil, fmt.Errorf("failed to get available slots: %w", err) } return slots, nil } // CreateBooking creates a new booking with Jitsi meeting integration func (s *bookingService) CreateBooking(userID uint, req BookingRequest) (*models.Booking, error) { // Get the schedule to validate availability schedule, err := s.scheduleRepo.GetByID(req.ScheduleID) if err != nil { log.Printf("Failed to get schedule %d: %v", req.ScheduleID, err) return nil, fmt.Errorf("invalid schedule: %w", err) } // Check if the schedule is available if !schedule.IsAvailable || schedule.BookedCount >= schedule.MaxBookings { return nil, fmt.Errorf("schedule slot is not available") } // Create the booking record first (without Jitsi details) booking := &models.Booking{ UserID: userID, ScheduledAt: schedule.StartTime, Duration: 60, // Default duration, can be made configurable Status: models.BookingStatusScheduled, Amount: req.Amount, Notes: req.Notes, PaymentStatus: models.PaymentStatusPending, } // Save the booking to get an ID if err := s.bookingRepo.Create(booking); err != nil { log.Printf("Failed to create booking for user %d: %v", userID, err) return nil, fmt.Errorf("failed to create booking: %w", err) } // Create Jitsi meeting after successful booking creation jitsiMeeting, err := s.jitsiService.CreateMeeting(booking.ID, booking.ScheduledAt) if err != nil { log.Printf("Failed to create Jitsi meeting for booking %d: %v", booking.ID, err) // Don't fail the booking creation if Jitsi fails, but log the error // The meeting can be created later or manually log.Printf("Booking %d created without Jitsi meeting due to error: %v", booking.ID, err) } else { // Update booking with Jitsi meeting details booking.JitsiRoomID = jitsiMeeting.RoomID booking.JitsiRoomURL = jitsiMeeting.RoomURL if err := s.bookingRepo.Update(booking); err != nil { log.Printf("Failed to update booking %d with Jitsi details: %v", booking.ID, err) // Log error but don't fail the booking creation } else { log.Printf("Successfully created Jitsi meeting for booking %d: Room ID %s", booking.ID, jitsiMeeting.RoomID) } } // Increment the booked count for the schedule if err := s.scheduleRepo.IncrementBookedCount(req.ScheduleID); err != nil { log.Printf("Failed to increment booked count for schedule %d: %v", req.ScheduleID, err) // This is not critical, continue with the booking } // Send meeting information notification if Jitsi meeting was created successfully if booking.JitsiRoomID != "" && booking.JitsiRoomURL != "" { user, err := s.userRepo.GetByID(userID) if err != nil { log.Printf("Failed to get user %d for notification: %v", userID, err) } else { if err := s.notificationService.SendMeetingInfo(user, booking); err != nil { log.Printf("Failed to send meeting info notification to user %d: %v", userID, err) } } } return booking, nil } // GetUserBookings retrieves all bookings for a specific user func (s *bookingService) GetUserBookings(userID uint) ([]models.Booking, error) { bookings, err := s.bookingRepo.GetByUserID(userID) if err != nil { log.Printf("Failed to get bookings for user %d: %v", userID, err) return nil, fmt.Errorf("failed to get user bookings: %w", err) } return bookings, nil } // CancelBooking cancels a booking and cleans up associated resources func (s *bookingService) CancelBooking(userID, bookingID uint) error { // Get the booking to validate ownership and status booking, err := s.bookingRepo.GetByID(bookingID) if err != nil { log.Printf("Failed to get booking %d: %v", bookingID, err) return fmt.Errorf("booking not found: %w", err) } // Validate ownership if booking.UserID != userID { return fmt.Errorf("unauthorized: booking does not belong to user") } // Check if booking can be cancelled if !booking.CanBeCancelled() { return fmt.Errorf("booking cannot be cancelled") } // Update booking status booking.Status = models.BookingStatusCancelled if err := s.bookingRepo.Update(booking); err != nil { log.Printf("Failed to update booking %d status to cancelled: %v", bookingID, err) return fmt.Errorf("failed to cancel booking: %w", err) } // Clean up Jitsi meeting if it exists if booking.JitsiRoomID != "" { if err := s.jitsiService.DeleteMeeting(booking.JitsiRoomID); err != nil { log.Printf("Failed to delete Jitsi meeting %s for cancelled booking %d: %v", booking.JitsiRoomID, bookingID, err) // Don't fail the cancellation if Jitsi cleanup fails } } log.Printf("Successfully cancelled booking %d for user %d", bookingID, userID) return nil } // RescheduleBooking reschedules a booking to a new time slot func (s *bookingService) RescheduleBooking(userID, bookingID uint, newScheduleID uint) error { // Get the existing booking booking, err := s.bookingRepo.GetByID(bookingID) if err != nil { log.Printf("Failed to get booking %d: %v", bookingID, err) return fmt.Errorf("booking not found: %w", err) } // Validate ownership if booking.UserID != userID { return fmt.Errorf("unauthorized: booking does not belong to user") } // Check if booking can be rescheduled if !booking.CanBeRescheduled() { return fmt.Errorf("booking cannot be rescheduled") } // Get the new schedule newSchedule, err := s.scheduleRepo.GetByID(newScheduleID) if err != nil { log.Printf("Failed to get new schedule %d: %v", newScheduleID, err) return fmt.Errorf("invalid new schedule: %w", err) } // Check if the new schedule is available if !newSchedule.IsAvailable || newSchedule.BookedCount >= newSchedule.MaxBookings { return fmt.Errorf("new schedule slot is not available") } // Clean up old Jitsi meeting if booking.JitsiRoomID != "" { if err := s.jitsiService.DeleteMeeting(booking.JitsiRoomID); err != nil { log.Printf("Failed to delete old Jitsi meeting %s: %v", booking.JitsiRoomID, err) } } // Create new Jitsi meeting jitsiMeeting, err := s.jitsiService.CreateMeeting(booking.ID, newSchedule.StartTime) if err != nil { log.Printf("Failed to create new Jitsi meeting for rescheduled booking %d: %v", bookingID, err) // Continue with rescheduling even if Jitsi fails booking.JitsiRoomID = "" booking.JitsiRoomURL = "" } else { booking.JitsiRoomID = jitsiMeeting.RoomID booking.JitsiRoomURL = jitsiMeeting.RoomURL } // Update booking with new schedule details booking.ScheduledAt = newSchedule.StartTime if err := s.bookingRepo.Update(booking); err != nil { log.Printf("Failed to update rescheduled booking %d: %v", bookingID, err) return fmt.Errorf("failed to reschedule booking: %w", err) } // Update schedule counts if err := s.scheduleRepo.IncrementBookedCount(newScheduleID); err != nil { log.Printf("Failed to increment booked count for new schedule %d: %v", newScheduleID, err) } log.Printf("Successfully rescheduled booking %d for user %d to schedule %d", bookingID, userID, newScheduleID) return nil }