| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- // 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")
- // Подтверждаем только бронь в статусе 'pending': нельзя реанимировать
- // отменённую бронь или подтвердить несуществующую.
- ok, err := h.bookingRepo.UpdateStatusIfPending(r.Context(), id, "confirmed")
- if err != nil {
- writeError(w, http.StatusInternalServerError, "failed to confirm booking", err)
- return
- }
- if !ok {
- writeError(w, http.StatusConflict, "booking cannot be confirmed in its current state", nil)
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "confirmed"})
- }
|