backend-service/internal/handlers/booking.go

341 lines
8.7 KiB
Go
Raw Permalink Normal View History

package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"attune-heart-therapy/internal/middleware"
"attune-heart-therapy/internal/models"
"attune-heart-therapy/internal/repositories"
"attune-heart-therapy/internal/services"
"github.com/gin-gonic/gin"
)
type BookingHandler struct {
bookingService services.BookingService
userRepo repositories.UserRepository
jitsiService services.JitsiService
}
func NewBookingHandler(bookingService services.BookingService, userRepo repositories.UserRepository, jitsiService services.JitsiService) *BookingHandler {
return &BookingHandler{
bookingService: bookingService,
userRepo: userRepo,
jitsiService: jitsiService,
}
}
// BookingResponse represents a booking with personalized meeting link
type BookingResponse struct {
models.Booking
PersonalizedMeetingURL string `json:"personalized_meeting_url,omitempty"`
}
// addPersonalizedLink adds a personalized meeting link to a booking
func (h *BookingHandler) addPersonalizedLink(booking *models.Booking, user *models.User) BookingResponse {
response := BookingResponse{
Booking: *booking,
}
// Only add personalized link if Jitsi URL exists
if booking.JitsiRoomURL != "" && booking.JitsiRoomID != "" {
displayName := fmt.Sprintf("%s %s", user.FirstName, user.LastName)
if displayName == " " || displayName == "" {
displayName = user.Email
}
// Generate JWT token for Jitsi authentication
token, err := h.jitsiService.GenerateJitsiToken(
booking.JitsiRoomID,
displayName,
user.Email,
user.IsAdmin,
)
var personalizedURL string
if err == nil && token != "" {
// Use JWT token if available
personalizedURL = fmt.Sprintf("%s?jwt=%s", booking.JitsiRoomURL, token)
} else {
// Fallback to URL parameters
personalizedURL = fmt.Sprintf("%s#userInfo.displayName=\"%s\"", booking.JitsiRoomURL, displayName)
if user.IsAdmin {
personalizedURL = fmt.Sprintf("%s&config.startWithAudioMuted=false&config.startWithVideoMuted=false", personalizedURL)
}
}
response.PersonalizedMeetingURL = personalizedURL
}
return response
}
// GetAvailableSlots handles GET /api/schedules for available slots
func (h *BookingHandler) GetAvailableSlots(c *gin.Context) {
// Get date parameter from query string
dateStr := c.Query("date")
if dateStr == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Date parameter is required (format: YYYY-MM-DD)",
})
return
}
// Parse the date
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid date format. Expected: YYYY-MM-DD",
"details": err.Error(),
})
return
}
// Get available slots
slots, err := h.bookingService.GetAvailableSlots(date)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get available slots",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"date": dateStr,
"slots": slots,
})
}
// CreateBooking handles POST /api/bookings for booking creation
func (h *BookingHandler) CreateBooking(c *gin.Context) {
// Get user ID from JWT token (set by auth middleware)
userID, exists := middleware.GetUserIDFromContext(c)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "User not authenticated",
})
return
}
var req services.BookingRequest
// Bind JSON request to struct
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request format",
"details": err.Error(),
})
return
}
// Create the booking
booking, err := h.bookingService.CreateBooking(userID, req)
if err != nil {
// Handle specific error cases
if err.Error() == "schedule slot is not available" {
c.JSON(http.StatusConflict, gin.H{
"error": "The selected time slot is no longer available",
})
return
}
if err.Error() == "invalid schedule" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid schedule ID provided",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create booking",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Booking created successfully",
"booking": booking,
})
}
// GetUserBookings handles GET /api/bookings for user's booking history
func (h *BookingHandler) GetUserBookings(c *gin.Context) {
// Get user ID from JWT token (set by auth middleware)
userID, exists := middleware.GetUserIDFromContext(c)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "User not authenticated",
})
return
}
// Get user details
user, err := h.userRepo.GetByID(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get user information",
"details": err.Error(),
})
return
}
// Get user's bookings
bookings, err := h.bookingService.GetUserBookings(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get user bookings",
"details": err.Error(),
})
return
}
// Add personalized meeting links to each booking
bookingResponses := make([]BookingResponse, len(bookings))
for i, booking := range bookings {
bookingResponses[i] = h.addPersonalizedLink(&booking, user)
}
c.JSON(http.StatusOK, gin.H{
"bookings": bookingResponses,
})
}
// CancelBooking handles PUT /api/bookings/:id/cancel for booking cancellation
func (h *BookingHandler) CancelBooking(c *gin.Context) {
// Get user ID from JWT token (set by auth middleware)
userID, exists := middleware.GetUserIDFromContext(c)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "User not authenticated",
})
return
}
// Get booking ID from URL parameter
bookingIDStr := c.Param("id")
bookingID, err := strconv.ParseUint(bookingIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid booking ID",
})
return
}
// Cancel the booking
if err := h.bookingService.CancelBooking(userID, uint(bookingID)); err != nil {
// Handle specific error cases
if err.Error() == "unauthorized: booking does not belong to user" {
c.JSON(http.StatusForbidden, gin.H{
"error": "You can only cancel your own bookings",
})
return
}
if err.Error() == "booking cannot be cancelled" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "This booking cannot be cancelled (must be at least 24 hours before scheduled time)",
})
return
}
if err.Error() == "booking not found" {
c.JSON(http.StatusNotFound, gin.H{
"error": "Booking not found",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to cancel booking",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Booking cancelled successfully",
})
}
// RescheduleBooking handles PUT /api/bookings/:id/reschedule for booking rescheduling
func (h *BookingHandler) RescheduleBooking(c *gin.Context) {
// Get user ID from JWT token (set by auth middleware)
userID, exists := middleware.GetUserIDFromContext(c)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "User not authenticated",
})
return
}
// Get booking ID from URL parameter
bookingIDStr := c.Param("id")
bookingID, err := strconv.ParseUint(bookingIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid booking ID",
})
return
}
var req struct {
NewScheduleID uint `json:"new_schedule_id" binding:"required"`
}
// Bind JSON request to struct
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request format",
"details": err.Error(),
})
return
}
// Reschedule the booking
if err := h.bookingService.RescheduleBooking(userID, uint(bookingID), req.NewScheduleID); err != nil {
// Handle specific error cases
if err.Error() == "unauthorized: booking does not belong to user" {
c.JSON(http.StatusForbidden, gin.H{
"error": "You can only reschedule your own bookings",
})
return
}
if err.Error() == "booking cannot be rescheduled" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "This booking cannot be rescheduled (must be at least 2 hours before scheduled time)",
})
return
}
if err.Error() == "new schedule slot is not available" {
c.JSON(http.StatusConflict, gin.H{
"error": "The new time slot is not available",
})
return
}
if err.Error() == "booking not found" || err.Error() == "invalid new schedule" {
c.JSON(http.StatusNotFound, gin.H{
"error": "Booking or new schedule not found",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to reschedule booking",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Booking rescheduled successfully",
})
}