// Package handlers package handlers import ( "encoding/json" "errors" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgconn" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/middleware" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/repository" "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/validator" ) type BookingHandler struct { bookingRepo *repository.BookingRepo } func NewBookingHandler(bookingRepo *repository.BookingRepo) *BookingHandler { return &BookingHandler{bookingRepo: bookingRepo} } type createBookingRequest struct { PlaceID string `json:"place_id" validate:"required,uuid"` StartTime string `json:"start_time" validate:"required,datetime=2006-01-02T15:04:05Z07:00"` EndTime string `json:"end_time" validate:"required,datetime=2006-01-02T15:04:05Z07:00"` Comment *string `json:"comment" validate:"omitempty,max=1000"` } func (h *BookingHandler) Create(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) var req createBookingRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body", err) return } if err := validator.Validate(req); err != nil { writeValidationError(w, err) return } start, err := time.Parse(time.RFC3339, req.StartTime) if err != nil { writeError(w, http.StatusBadRequest, "invalid start_time format, expected RFC3339", err) return } end, err := time.Parse(time.RFC3339, req.EndTime) if err != nil { writeError(w, http.StatusBadRequest, "invalid end_time format, expected RFC3339", err) return } if !end.After(start) { writeError(w, http.StatusBadRequest, "end_time must be after start_time", nil) return } minHours, err := h.bookingRepo.GetPlaceMinHours(r.Context(), req.PlaceID) if err != nil { if errors.Is(err, repository.ErrPlaceNotFound) { writeError(w, http.StatusNotFound, "place not found", nil) return } writeError(w, http.StatusInternalServerError, "failed to validate booking", err) return } if end.Sub(start) < time.Duration(minHours)*time.Hour { writeError(w, http.StatusBadRequest, "booking duration is less than minimum hours required", nil) return } booking := &models.Booking{ PlaceID: req.PlaceID, UserID: userID, StartTime: start, EndTime: end, Comment: req.Comment, } if err := h.bookingRepo.Create(r.Context(), booking); err != nil { if errors.Is(err, repository.ErrPlaceNotFound) { writeError(w, http.StatusNotFound, "place not found", nil) return } var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23P01" { // exclusion_constraint_violation writeError(w, http.StatusConflict, "time slot is not available", err) return } writeError(w, http.StatusInternalServerError, "failed to create booking", err) return } writeJSON(w, http.StatusCreated, booking) } func (h *BookingHandler) ListMy(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) bookings, err := h.bookingRepo.ListByUser(r.Context(), userID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to list bookings", err) return } if bookings == nil { bookings = []*models.Booking{} } writeJSON(w, http.StatusOK, map[string]interface{}{"data": bookings}) } func (h *BookingHandler) Cancel(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) booking, err := h.bookingRepo.GetByID(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, "failed to get booking", err) return } if booking == nil { writeError(w, http.StatusNotFound, "booking not found", nil) return } if booking.UserID != userID { writeError(w, http.StatusForbidden, "not your booking", nil) return } ok, err := h.bookingRepo.UpdateStatusIfPending(r.Context(), id, "cancelled") if err != nil { writeError(w, http.StatusInternalServerError, "failed to cancel booking", err) return } if !ok { writeError(w, http.StatusConflict, "booking cannot be cancelled in its current state", nil) return } writeJSON(w, http.StatusOK, map[string]string{"status": "cancelled"}) } func (h *BookingHandler) Confirm(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.bookingRepo.UpdateStatus(r.Context(), id, "confirmed"); err != nil { writeError(w, http.StatusInternalServerError, "failed to confirm booking", err) return } writeJSON(w, http.StatusOK, map[string]string{"status": "confirmed"}) }