// Package handlers package handlers import ( "encoding/json" "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/photoplaces/backend/internal/middleware" "github.com/photoplaces/backend/internal/models" "github.com/photoplaces/backend/internal/services" "github.com/photoplaces/backend/internal/validator" ) type PlaceHandler struct { placeSvc *services.PlaceService } func NewPlaceHandler(placeSvc *services.PlaceService) *PlaceHandler { return &PlaceHandler{placeSvc: placeSvc} } func (h *PlaceHandler) List(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() filter := models.PlaceFilter{ Type: q.Get("type"), Status: "published", } if tags := q["tags"]; len(tags) > 0 { filter.TagIDs = tags } if features := q["features"]; len(features) > 0 { filter.FeatureIDs = features } if mr := q.Get("min_rating"); mr != "" { if v, err := strconv.ParseFloat(mr, 64); err == nil { filter.MinRating = v } } if pmin := q.Get("price_min"); pmin != "" { if v, err := strconv.Atoi(pmin); err == nil { filter.PriceMin = &v } } if pmax := q.Get("price_max"); pmax != "" { if v, err := strconv.Atoi(pmax); err == nil { filter.PriceMax = &v } } if bounds := q.Get("bounds"); bounds != "" { var b models.Bounds if _, err := fmt.Sscanf(bounds, "%f,%f,%f,%f", &b.SWLat, &b.SWLng, &b.NELat, &b.NELng); err == nil { filter.Bounds = &b } } if limit := q.Get("limit"); limit != "" { if v, err := strconv.Atoi(limit); err == nil { filter.Limit_ = v } } filter.Sort = q.Get("sort") if q.Get("user_lat") != "" && q.Get("user_lng") != "" { if lat, err := strconv.ParseFloat(q.Get("user_lat"), 64); err == nil { filter.UserLat = &lat } if lng, err := strconv.ParseFloat(q.Get("user_lng"), 64); err == nil { filter.UserLng = &lng } } places, err := h.placeSvc.List(r.Context(), filter) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if places == nil { places = []*models.Place{} } writeJSON(w, http.StatusOK, map[string]interface{}{ "data": places, }) } func (h *PlaceHandler) GetByID(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") place, err := h.placeSvc.GetByID(r.Context(), id, true) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if place == nil { writeError(w, http.StatusNotFound, "place not found") return } writeJSON(w, http.StatusOK, place) } type createPlaceRequest struct { Title string `json:"title" validate:"required,min=1,max=255"` Description *string `json:"description" validate:"omitempty,max=5000"` Address *string `json:"address" validate:"omitempty,max=500"` Lat float64 `json:"lat" validate:"required,latitude"` Lng float64 `json:"lng" validate:"required,longitude"` Type string `json:"type" validate:"required,place_type"` AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"` CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"` Tags []string `json:"tags" validate:"dive,slug,max=50"` Features []string `json:"features" validate:"dive,slug,max=50"` HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"` Currency string `json:"currency" validate:"omitempty,currency,len=3"` MinHours int `json:"min_hours" validate:"min=0,max=100"` } type updatePlaceRequest struct { Title *string `json:"title" validate:"omitempty,min=1,max=255"` Description *string `json:"description" validate:"omitempty,max=5000"` Address *string `json:"address" validate:"omitempty,max=500"` Lat *float64 `json:"lat" validate:"omitempty,latitude"` Lng *float64 `json:"lng" validate:"omitempty,longitude"` AccessInfo *string `json:"access_info" validate:"omitempty,max=2000"` CoverImage *string `json:"cover_image" validate:"omitempty,url,max=500"` Tags []string `json:"tags" validate:"dive,slug,max=50"` Features []string `json:"features" validate:"dive,slug,max=50"` HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"` Currency *string `json:"currency" validate:"omitempty,currency,len=3"` MinHours *int `json:"min_hours" validate:"omitempty,min=0,max=100"` } type moderatePlaceRequest struct { Action string `json:"action" validate:"required,oneof=approve reject"` Comment *string `json:"comment" validate:"omitempty,max=1000"` } func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) role := middleware.GetUserRole(r.Context()) var req createPlaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := validator.Validate(req); err != nil { writeValidationError(w, err) return } if req.Type == "studio" && role != "landlord" && role != "moderator" && role != "superadmin" { writeError(w, http.StatusForbidden, "only landlords can create studios") return } if req.Type == "place" && role != "customer" && role != "moderator" && role != "superadmin" { writeError(w, http.StatusForbidden, "only customers can create places") return } tags := make([]models.Tag, len(req.Tags)) for i, t := range req.Tags { tags[i] = models.Tag{ID: t} } features := make([]models.Feature, len(req.Features)) for i, f := range req.Features { features[i] = models.Feature{ID: f} } place, err := h.placeSvc.Create(r.Context(), services.CreatePlaceInput{ OwnerID: userID, Type: req.Type, Title: req.Title, Description: req.Description, Address: req.Address, Lat: req.Lat, Lng: req.Lng, CoverImage: req.CoverImage, AccessInfo: req.AccessInfo, Tags: tags, Features: features, HourlyRate: req.HourlyRate, Currency: req.Currency, MinHours: req.MinHours, }) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, place) } func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) role := middleware.GetUserRole(r.Context()) var req updatePlaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := validator.Validate(req); err != nil { writeValidationError(w, err) return } isModerator := role == "moderator" || role == "superadmin" input := services.UpdatePlaceInput{ ID: id, OwnerID: userID, } if req.Title != nil { input.Title = req.Title } input.Description = req.Description input.Address = req.Address input.Lat = req.Lat input.Lng = req.Lng input.AccessInfo = req.AccessInfo input.HourlyRate = req.HourlyRate input.Currency = req.Currency input.MinHours = req.MinHours place, err := h.placeSvc.Update(r.Context(), input, isModerator) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if place == nil { writeError(w, http.StatusNotFound, "place not found") return } writeJSON(w, http.StatusOK, place) } func (h *PlaceHandler) Moderate(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) var req moderatePlaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := validator.Validate(req); err != nil { writeValidationError(w, err) return } if err := h.placeSvc.Moderate(r.Context(), id, req.Action, req.Comment, userID); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (h *PlaceHandler) Delete(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") userID := middleware.GetUserID(r.Context()) role := middleware.GetUserRole(r.Context()) place, err := h.placeSvc.GetByID(r.Context(), id, false) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if place == nil { writeError(w, http.StatusNotFound, "place not found") return } if place.OwnerID != userID && role != "moderator" && role != "superadmin" { writeError(w, http.StatusForbidden, "not your place") return } if err := h.placeSvc.Delete(r.Context(), id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.WriteHeader(http.StatusNoContent) }