backend-service/internal/services/booking_service.go
ats-tech25 b8dd31b449 feat(services): Implement comprehensive booking service with Jitsi integration
- Add BookingService with core booking management functionality
- Implement mock repositories for testing booking service interactions
- Create booking integration test with Jitsi room generation
- Add methods for creating, retrieving, and managing bookings
- Integrate Jitsi service for generating meeting room URLs
- Implement schedule management within booking service
- Add support for booking creation with user and schedule context
- Enhance database layer to support repository retrieval
Closes #TICKET_NUMBER (if applicable)
2025-11-05 15:42:59 +00:00

223 lines
7.5 KiB
Go

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
jitsiService JitsiService
paymentService PaymentService
}
// NewBookingService creates a new instance of BookingService
func NewBookingService(
bookingRepo repositories.BookingRepository,
scheduleRepo repositories.ScheduleRepository,
jitsiService JitsiService,
paymentService PaymentService,
) BookingService {
return &bookingService{
bookingRepo: bookingRepo,
scheduleRepo: scheduleRepo,
jitsiService: jitsiService,
paymentService: paymentService,
}
}
// 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
}
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
}