- Add new `app` package to manage application initialization and lifecycle - Refactor `main.go` to use new application management approach - Implement graceful shutdown with context timeout and signal handling - Add dependency injection container initialization - Enhance logging with configurable log levels and structured logging - Update configuration loading and server initialization process - Modify Jitsi configuration in `.env` for custom deployment - Improve error handling and logging throughout application startup - Centralize application startup and shutdown logic in single package Introduces a more robust and flexible application management system with improved initialization, logging, and shutdown capabilities.
279 lines
9.9 KiB
Go
279 lines
9.9 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
|
|
userRepo repositories.UserRepository
|
|
jitsiService JitsiService
|
|
paymentService PaymentService
|
|
notificationService NotificationService
|
|
jobManagerService JobManagerService
|
|
}
|
|
|
|
// NewBookingService creates a new instance of BookingService
|
|
func NewBookingService(
|
|
bookingRepo repositories.BookingRepository,
|
|
scheduleRepo repositories.ScheduleRepository,
|
|
userRepo repositories.UserRepository,
|
|
jitsiService JitsiService,
|
|
paymentService PaymentService,
|
|
notificationService NotificationService,
|
|
jobManagerService JobManagerService,
|
|
) BookingService {
|
|
return &bookingService{
|
|
bookingRepo: bookingRepo,
|
|
scheduleRepo: scheduleRepo,
|
|
userRepo: userRepo,
|
|
jitsiService: jitsiService,
|
|
paymentService: paymentService,
|
|
notificationService: notificationService,
|
|
jobManagerService: jobManagerService,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Schedule reminder notifications for the booking
|
|
if s.jobManagerService != nil && s.jobManagerService.IsRunning() {
|
|
if err := s.jobManagerService.ScheduleRemindersForBooking(booking.ID, userID, booking.ScheduledAt); err != nil {
|
|
log.Printf("Failed to schedule reminders for booking %d: %v", booking.ID, err)
|
|
// Don't fail the booking creation if reminder scheduling fails
|
|
} else {
|
|
log.Printf("Successfully scheduled reminders for booking %d", booking.ID)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Cancel scheduled reminders for the booking
|
|
if s.jobManagerService != nil && s.jobManagerService.IsRunning() {
|
|
if err := s.jobManagerService.CancelRemindersForBooking(bookingID); err != nil {
|
|
log.Printf("Failed to cancel reminders for booking %d: %v", bookingID, err)
|
|
// Don't fail the cancellation if reminder cleanup fails
|
|
} else {
|
|
log.Printf("Successfully cancelled reminders for booking %d", bookingID)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Cancel old reminders and schedule new ones
|
|
if s.jobManagerService != nil && s.jobManagerService.IsRunning() {
|
|
// Cancel existing reminders
|
|
if err := s.jobManagerService.CancelRemindersForBooking(bookingID); err != nil {
|
|
log.Printf("Failed to cancel old reminders for rescheduled booking %d: %v", bookingID, err)
|
|
}
|
|
|
|
// Schedule new reminders with the new meeting time
|
|
if err := s.jobManagerService.ScheduleRemindersForBooking(bookingID, userID, booking.ScheduledAt); err != nil {
|
|
log.Printf("Failed to schedule new reminders for rescheduled booking %d: %v", bookingID, err)
|
|
} else {
|
|
log.Printf("Successfully rescheduled reminders for booking %d", bookingID)
|
|
}
|
|
}
|
|
|
|
log.Printf("Successfully rescheduled booking %d for user %d to schedule %d", bookingID, userID, newScheduleID)
|
|
return nil
|
|
}
|