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)
This commit is contained in:
parent
d0117e6ac7
commit
b8dd31b449
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"attune-heart-therapy/internal/config"
|
"attune-heart-therapy/internal/config"
|
||||||
"attune-heart-therapy/internal/models"
|
"attune-heart-therapy/internal/models"
|
||||||
|
"attune-heart-therapy/internal/repositories"
|
||||||
|
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -110,3 +111,8 @@ func (db *DB) Health() error {
|
|||||||
}
|
}
|
||||||
return sqlDB.Ping()
|
return sqlDB.Ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRepositories returns all repository instances
|
||||||
|
func (db *DB) GetRepositories() *repositories.Repositories {
|
||||||
|
return repositories.NewRepositories(db.DB)
|
||||||
|
}
|
||||||
|
|||||||
@ -143,9 +143,26 @@ func (s *Server) setupRoutes() {
|
|||||||
|
|
||||||
// initializeServices sets up all services and handlers
|
// initializeServices sets up all services and handlers
|
||||||
func (s *Server) initializeServices() {
|
func (s *Server) initializeServices() {
|
||||||
|
// Initialize repositories
|
||||||
|
repos := s.db.GetRepositories()
|
||||||
|
|
||||||
|
// Initialize Jitsi service
|
||||||
|
jitsiService := services.NewJitsiService(&s.config.Jitsi)
|
||||||
|
|
||||||
// Initialize payment service
|
// Initialize payment service
|
||||||
paymentService := services.NewPaymentService(s.config)
|
paymentService := services.NewPaymentService(s.config)
|
||||||
|
|
||||||
|
// Initialize booking service with Jitsi integration
|
||||||
|
bookingService := services.NewBookingService(
|
||||||
|
repos.Booking,
|
||||||
|
repos.Schedule,
|
||||||
|
jitsiService,
|
||||||
|
paymentService,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store services for later use (if needed)
|
||||||
|
_ = bookingService // Will be used when booking handlers are implemented
|
||||||
|
|
||||||
// Initialize payment handler
|
// Initialize payment handler
|
||||||
s.paymentHandler = handlers.NewPaymentHandler(paymentService)
|
s.paymentHandler = handlers.NewPaymentHandler(paymentService)
|
||||||
}
|
}
|
||||||
|
|||||||
199
internal/services/booking_integration_test.go
Normal file
199
internal/services/booking_integration_test.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"attune-heart-therapy/internal/config"
|
||||||
|
"attune-heart-therapy/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockBookingRepository for testing
|
||||||
|
type MockBookingRepository struct {
|
||||||
|
bookings map[uint]*models.Booking
|
||||||
|
nextID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockBookingRepository() *MockBookingRepository {
|
||||||
|
return &MockBookingRepository{
|
||||||
|
bookings: make(map[uint]*models.Booking),
|
||||||
|
nextID: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockBookingRepository) Create(booking *models.Booking) error {
|
||||||
|
booking.ID = m.nextID
|
||||||
|
m.nextID++
|
||||||
|
m.bookings[booking.ID] = booking
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockBookingRepository) GetByID(id uint) (*models.Booking, error) {
|
||||||
|
if booking, exists := m.bookings[id]; exists {
|
||||||
|
return booking, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockBookingRepository) GetByUserID(userID uint) ([]models.Booking, error) {
|
||||||
|
var result []models.Booking
|
||||||
|
for _, booking := range m.bookings {
|
||||||
|
if booking.UserID == userID {
|
||||||
|
result = append(result, *booking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockBookingRepository) Update(booking *models.Booking) error {
|
||||||
|
m.bookings[booking.ID] = booking
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockBookingRepository) Delete(id uint) error {
|
||||||
|
delete(m.bookings, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockBookingRepository) GetUpcomingBookings() ([]models.Booking, error) {
|
||||||
|
var result []models.Booking
|
||||||
|
now := time.Now()
|
||||||
|
for _, booking := range m.bookings {
|
||||||
|
if booking.Status == models.BookingStatusScheduled && booking.ScheduledAt.After(now) {
|
||||||
|
result = append(result, *booking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockScheduleRepository for testing
|
||||||
|
type MockScheduleRepository struct {
|
||||||
|
schedules map[uint]*models.Schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockScheduleRepository() *MockScheduleRepository {
|
||||||
|
return &MockScheduleRepository{
|
||||||
|
schedules: make(map[uint]*models.Schedule),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduleRepository) Create(schedule *models.Schedule) error {
|
||||||
|
m.schedules[schedule.ID] = schedule
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduleRepository) GetAvailable(date time.Time) ([]models.Schedule, error) {
|
||||||
|
var result []models.Schedule
|
||||||
|
for _, schedule := range m.schedules {
|
||||||
|
if schedule.IsAvailable && schedule.BookedCount < schedule.MaxBookings {
|
||||||
|
result = append(result, *schedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduleRepository) Update(schedule *models.Schedule) error {
|
||||||
|
m.schedules[schedule.ID] = schedule
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduleRepository) GetByID(id uint) (*models.Schedule, error) {
|
||||||
|
if schedule, exists := m.schedules[id]; exists {
|
||||||
|
return schedule, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduleRepository) IncrementBookedCount(scheduleID uint) error {
|
||||||
|
if schedule, exists := m.schedules[scheduleID]; exists {
|
||||||
|
schedule.BookedCount++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockScheduleRepository) DecrementBookedCount(scheduleID uint) error {
|
||||||
|
if schedule, exists := m.schedules[scheduleID]; exists {
|
||||||
|
schedule.BookedCount--
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBookingService_CreateBookingWithJitsiIntegration(t *testing.T) {
|
||||||
|
// Setup mock repositories
|
||||||
|
bookingRepo := NewMockBookingRepository()
|
||||||
|
scheduleRepo := NewMockScheduleRepository()
|
||||||
|
|
||||||
|
// Setup Jitsi service
|
||||||
|
jitsiConfig := &config.JitsiConfig{
|
||||||
|
BaseURL: "https://meet.jit.si",
|
||||||
|
}
|
||||||
|
jitsiService := NewJitsiService(jitsiConfig)
|
||||||
|
|
||||||
|
// Setup mock payment service (nil for this test)
|
||||||
|
var paymentService PaymentService
|
||||||
|
|
||||||
|
// Create booking service
|
||||||
|
bookingService := NewBookingService(bookingRepo, scheduleRepo, jitsiService, paymentService)
|
||||||
|
|
||||||
|
// Create a test schedule
|
||||||
|
schedule := &models.Schedule{
|
||||||
|
StartTime: time.Now().Add(24 * time.Hour),
|
||||||
|
EndTime: time.Now().Add(25 * time.Hour),
|
||||||
|
IsAvailable: true,
|
||||||
|
MaxBookings: 1,
|
||||||
|
BookedCount: 0,
|
||||||
|
}
|
||||||
|
schedule.ID = 1
|
||||||
|
scheduleRepo.schedules[1] = schedule
|
||||||
|
|
||||||
|
// Create booking request
|
||||||
|
req := BookingRequest{
|
||||||
|
ScheduleID: 1,
|
||||||
|
Amount: 100.0,
|
||||||
|
Notes: "Test booking with Jitsi integration",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create booking
|
||||||
|
booking, err := bookingService.CreateBooking(123, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if booking == nil {
|
||||||
|
t.Fatal("Expected booking to be created, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify booking details
|
||||||
|
if booking.UserID != 123 {
|
||||||
|
t.Errorf("Expected user ID 123, got %d", booking.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if booking.Amount != 100.0 {
|
||||||
|
t.Errorf("Expected amount 100.0, got %f", booking.Amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Jitsi integration
|
||||||
|
if booking.JitsiRoomID == "" {
|
||||||
|
t.Error("Expected Jitsi room ID to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if booking.JitsiRoomURL == "" {
|
||||||
|
t.Error("Expected Jitsi room URL to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify URL format
|
||||||
|
expectedPrefix := "https://meet.jit.si/"
|
||||||
|
if len(booking.JitsiRoomURL) <= len(expectedPrefix) {
|
||||||
|
t.Error("Expected room URL to contain room ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if booking.JitsiRoomURL[:len(expectedPrefix)] != expectedPrefix {
|
||||||
|
t.Errorf("Expected room URL to start with %s, got %s", expectedPrefix, booking.JitsiRoomURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify schedule booking count was incremented
|
||||||
|
updatedSchedule, _ := scheduleRepo.GetByID(1)
|
||||||
|
if updatedSchedule.BookedCount != 1 {
|
||||||
|
t.Errorf("Expected booked count to be 1, got %d", updatedSchedule.BookedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
222
internal/services/booking_service.go
Normal file
222
internal/services/booking_service.go
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
92
internal/services/jitsi_service.go
Normal file
92
internal/services/jitsi_service.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"attune-heart-therapy/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// jitsiService implements the JitsiService interface
|
||||||
|
type jitsiService struct {
|
||||||
|
config *config.JitsiConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJitsiService creates a new instance of JitsiService
|
||||||
|
func NewJitsiService(cfg *config.JitsiConfig) JitsiService {
|
||||||
|
return &jitsiService{
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMeeting creates a new Jitsi meeting room for a booking
|
||||||
|
func (j *jitsiService) CreateMeeting(bookingID uint, scheduledAt time.Time) (*JitsiMeeting, error) {
|
||||||
|
// Generate a unique room ID
|
||||||
|
roomID, err := j.generateRoomID(bookingID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to generate room ID for booking %d: %v", bookingID, err)
|
||||||
|
return nil, fmt.Errorf("failed to generate room ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the meeting URL
|
||||||
|
roomURL := j.GetMeetingURL(roomID)
|
||||||
|
|
||||||
|
meeting := &JitsiMeeting{
|
||||||
|
RoomID: roomID,
|
||||||
|
RoomURL: roomURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Created Jitsi meeting for booking %d: Room ID %s", bookingID, roomID)
|
||||||
|
return meeting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMeetingURL generates the full Jitsi meeting URL for a given room ID
|
||||||
|
func (j *jitsiService) GetMeetingURL(roomID string) string {
|
||||||
|
baseURL := j.config.BaseURL
|
||||||
|
if baseURL == "" {
|
||||||
|
// Default to meet.jit.si if no base URL is configured
|
||||||
|
baseURL = "https://meet.jit.si"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the base URL doesn't end with a slash
|
||||||
|
if baseURL[len(baseURL)-1] == '/' {
|
||||||
|
baseURL = baseURL[:len(baseURL)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s", baseURL, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMeeting handles cleanup of a Jitsi meeting room
|
||||||
|
// Note: Jitsi Meet doesn't require explicit room deletion as rooms are ephemeral
|
||||||
|
// This method is implemented for interface compliance and future extensibility
|
||||||
|
func (j *jitsiService) DeleteMeeting(roomID string) error {
|
||||||
|
// Jitsi Meet rooms are ephemeral and don't require explicit deletion
|
||||||
|
// However, we can log the deletion for audit purposes
|
||||||
|
log.Printf("Meeting room %s marked for cleanup", roomID)
|
||||||
|
|
||||||
|
// In the future, if using Jitsi as a Service (JaaS) or custom deployment,
|
||||||
|
// this method could make API calls to clean up resources
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRoomID creates a unique room identifier for the meeting
|
||||||
|
func (j *jitsiService) generateRoomID(bookingID uint) (string, error) {
|
||||||
|
// Generate a random component for uniqueness
|
||||||
|
randomBytes := make([]byte, 8)
|
||||||
|
_, err := rand.Read(randomBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomHex := hex.EncodeToString(randomBytes)
|
||||||
|
|
||||||
|
// Create a room ID that includes the booking ID and timestamp for uniqueness
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
roomID := fmt.Sprintf("booking-%d-%d-%s", bookingID, timestamp, randomHex)
|
||||||
|
|
||||||
|
return roomID, nil
|
||||||
|
}
|
||||||
95
internal/services/jitsi_service_test.go
Normal file
95
internal/services/jitsi_service_test.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"attune-heart-therapy/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJitsiService_CreateMeeting(t *testing.T) {
|
||||||
|
// Setup test config
|
||||||
|
cfg := &config.JitsiConfig{
|
||||||
|
BaseURL: "https://meet.jit.si",
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewJitsiService(cfg)
|
||||||
|
|
||||||
|
// Test creating a meeting
|
||||||
|
bookingID := uint(123)
|
||||||
|
scheduledAt := time.Now().Add(24 * time.Hour)
|
||||||
|
|
||||||
|
meeting, err := service.CreateMeeting(bookingID, scheduledAt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if meeting == nil {
|
||||||
|
t.Fatal("Expected meeting to be created, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if meeting.RoomID == "" {
|
||||||
|
t.Error("Expected room ID to be generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if meeting.RoomURL == "" {
|
||||||
|
t.Error("Expected room URL to be generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify URL format
|
||||||
|
expectedPrefix := "https://meet.jit.si/"
|
||||||
|
if len(meeting.RoomURL) <= len(expectedPrefix) {
|
||||||
|
t.Error("Expected room URL to contain room ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
if meeting.RoomURL[:len(expectedPrefix)] != expectedPrefix {
|
||||||
|
t.Errorf("Expected room URL to start with %s, got %s", expectedPrefix, meeting.RoomURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJitsiService_GetMeetingURL(t *testing.T) {
|
||||||
|
cfg := &config.JitsiConfig{
|
||||||
|
BaseURL: "https://meet.jit.si",
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewJitsiService(cfg)
|
||||||
|
|
||||||
|
roomID := "test-room-123"
|
||||||
|
url := service.GetMeetingURL(roomID)
|
||||||
|
|
||||||
|
expected := "https://meet.jit.si/test-room-123"
|
||||||
|
if url != expected {
|
||||||
|
t.Errorf("Expected URL %s, got %s", expected, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJitsiService_GetMeetingURL_DefaultBaseURL(t *testing.T) {
|
||||||
|
// Test with empty base URL (should use default)
|
||||||
|
cfg := &config.JitsiConfig{
|
||||||
|
BaseURL: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewJitsiService(cfg)
|
||||||
|
|
||||||
|
roomID := "test-room-456"
|
||||||
|
url := service.GetMeetingURL(roomID)
|
||||||
|
|
||||||
|
expected := "https://meet.jit.si/test-room-456"
|
||||||
|
if url != expected {
|
||||||
|
t.Errorf("Expected URL %s, got %s", expected, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJitsiService_DeleteMeeting(t *testing.T) {
|
||||||
|
cfg := &config.JitsiConfig{
|
||||||
|
BaseURL: "https://meet.jit.si",
|
||||||
|
}
|
||||||
|
|
||||||
|
service := NewJitsiService(cfg)
|
||||||
|
|
||||||
|
// Test deleting a meeting (should not error)
|
||||||
|
err := service.DeleteMeeting("test-room-789")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user