diff --git a/cmd/main.go b/cmd/main.go index edf5848..f8fe09a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,6 +11,9 @@ import ( "2024_2_ThereWillBeName/internal/pkg/places/delivery" placerepo "2024_2_ThereWillBeName/internal/pkg/places/repo" placeusecase "2024_2_ThereWillBeName/internal/pkg/places/usecase" + triphandler "2024_2_ThereWillBeName/internal/pkg/trips/delivery/http" + triprepo "2024_2_ThereWillBeName/internal/pkg/trips/repo" + tripusecase "2024_2_ThereWillBeName/internal/pkg/trips/usecase" "crypto/rand" "database/sql" "encoding/hex" @@ -39,7 +42,6 @@ func main() { newPlaceRepo := placerepo.NewPLaceRepository() placeUsecase := placeusecase.NewPlaceUsecase(newPlaceRepo) handler := delivery.NewPlacesHandler(placeUsecase) - logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) db, err := openDB(cfg.ConnStr) @@ -58,7 +60,9 @@ func main() { jwtHandler := jwt.NewJWT(string(jwtSecret)) authUseCase := usecase.NewAuthUsecase(authRepo, jwtHandler) h := httpHandler.NewAuthHandler(authUseCase, jwtHandler) - + tripsRepo := triprepo.NewTripRepository(db) + tripUsecase := tripusecase.NewTripsUsecase(tripsRepo) + tripHandler := triphandler.NewTripHandler(tripUsecase) corsMiddleware := middleware.NewCORSMiddleware([]string{cfg.AllowedOrigin}) r := mux.NewRouter().PathPrefix("/api/v1").Subrouter() @@ -77,9 +81,16 @@ func main() { auth.HandleFunc("/logout", h.Logout).Methods(http.MethodPost) users := r.PathPrefix("/users").Subrouter() users.Handle("/me", middleware.MiddlewareAuth(jwtHandler, http.HandlerFunc(h.CurrentUser))).Methods(http.MethodGet) + user := users.PathPrefix("/{userID}").Subrouter() places := r.PathPrefix("/places").Subrouter() places.HandleFunc("", handler.GetPlaceHandler).Methods(http.MethodGet) r.PathPrefix("/swagger").Handler(httpSwagger.WrapHandler) + trips := r.PathPrefix("/trips").Subrouter() + trips.HandleFunc("", tripHandler.CreateTripHandler).Methods(http.MethodPost) + trips.HandleFunc("/{id}", tripHandler.UpdateTripHandler).Methods(http.MethodPut) + trips.HandleFunc("/{id}", tripHandler.DeleteTripHandler).Methods(http.MethodDelete) + trips.HandleFunc("/{id}", tripHandler.GetTripHandler).Methods(http.MethodGet) + user.HandleFunc("/trips", tripHandler.GetTripsByUserIDHandler).Methods(http.MethodGet) srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.Port), Handler: r, diff --git a/docs/docs.go b/docs/docs.go index feef06c..45942e3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -167,6 +167,192 @@ const docTemplate = `{ } } }, + "/trips": { + "post": { + "description": "Create a new trip with given fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create a new trip", + "parameters": [ + { + "description": "Trip details", + "name": "trip", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Trip" + } + } + ], + "responses": { + "201": { + "description": "Trip created successfully", + "schema": { + "$ref": "#/definitions/models.Trip" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to create trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + } + }, + "/trips/{id}": { + "get": { + "description": "Get trip details by trip ID", + "produces": [ + "application/json" + ], + "summary": "Retrieve a trip by ID", + "parameters": [ + { + "type": "integer", + "description": "Trip ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Trip details", + "schema": { + "$ref": "#/definitions/models.Trip" + } + }, + "400": { + "description": "Invalid trip ID", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trip not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to retrieve trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + }, + "put": { + "description": "Update trip details by trip ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Update an existing trip", + "parameters": [ + { + "type": "integer", + "description": "Trip ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated trip details", + "name": "trip", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Trip" + } + } + ], + "responses": { + "200": { + "description": "Trip updated successfully", + "schema": { + "$ref": "#/definitions/models.Trip" + } + }, + "400": { + "description": "Invalid trip data", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trip not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to update trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a trip by trip ID", + "produces": [ + "application/json" + ], + "summary": "Delete a trip", + "parameters": [ + { + "type": "integer", + "description": "Trip ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Trip deleted successfully" + }, + "400": { + "description": "Invalid trip ID", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trip not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to delete trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + } + }, "/users/me": { "get": { "description": "Retrieve the current authenticated user information", @@ -195,6 +381,53 @@ const docTemplate = `{ } } } + }, + "/users/{userID}/trips": { + "get": { + "description": "Get all trips for a specific user", + "produces": [ + "application/json" + ], + "summary": "Retrieve trips by user ID", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "userID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of trips", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Trip" + } + } + }, + "400": { + "description": "Invalid user ID", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trips not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to retrieve trips", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -234,6 +467,38 @@ const docTemplate = `{ } } }, + "models.Trip": { + "type": "object", + "properties": { + "city_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "private": { + "type": "boolean" + }, + "start_date": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "models.User": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index dddcaa0..195e299 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -156,6 +156,192 @@ } } }, + "/trips": { + "post": { + "description": "Create a new trip with given fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create a new trip", + "parameters": [ + { + "description": "Trip details", + "name": "trip", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Trip" + } + } + ], + "responses": { + "201": { + "description": "Trip created successfully", + "schema": { + "$ref": "#/definitions/models.Trip" + } + }, + "400": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Invalid request", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to create trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + } + }, + "/trips/{id}": { + "get": { + "description": "Get trip details by trip ID", + "produces": [ + "application/json" + ], + "summary": "Retrieve a trip by ID", + "parameters": [ + { + "type": "integer", + "description": "Trip ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Trip details", + "schema": { + "$ref": "#/definitions/models.Trip" + } + }, + "400": { + "description": "Invalid trip ID", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trip not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to retrieve trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + }, + "put": { + "description": "Update trip details by trip ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Update an existing trip", + "parameters": [ + { + "type": "integer", + "description": "Trip ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated trip details", + "name": "trip", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Trip" + } + } + ], + "responses": { + "200": { + "description": "Trip updated successfully", + "schema": { + "$ref": "#/definitions/models.Trip" + } + }, + "400": { + "description": "Invalid trip data", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trip not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to update trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a trip by trip ID", + "produces": [ + "application/json" + ], + "summary": "Delete a trip", + "parameters": [ + { + "type": "integer", + "description": "Trip ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Trip deleted successfully" + }, + "400": { + "description": "Invalid trip ID", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trip not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to delete trip", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + } + }, "/users/me": { "get": { "description": "Retrieve the current authenticated user information", @@ -184,6 +370,53 @@ } } } + }, + "/users/{userID}/trips": { + "get": { + "description": "Get all trips for a specific user", + "produces": [ + "application/json" + ], + "summary": "Retrieve trips by user ID", + "parameters": [ + { + "type": "integer", + "description": "User ID", + "name": "userID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of trips", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Trip" + } + } + }, + "400": { + "description": "Invalid user ID", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "404": { + "description": "Trips not found", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + }, + "500": { + "description": "Failed to retrieve trips", + "schema": { + "$ref": "#/definitions/httpresponses.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -223,6 +456,38 @@ } } }, + "models.Trip": { + "type": "object", + "properties": { + "city_id": { + "type": "integer" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "private": { + "type": "boolean" + }, + "start_date": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "models.User": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1e3fba4..5ea5324 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -22,6 +22,27 @@ definitions: name: type: string type: object + models.Trip: + properties: + city_id: + type: integer + created_at: + type: string + description: + type: string + end_date: + type: string + id: + type: integer + name: + type: string + private: + type: boolean + start_date: + type: string + user_id: + type: integer + type: object models.User: properties: created_at: @@ -133,6 +154,160 @@ paths: schema: $ref: '#/definitions/httpresponses.ErrorResponse' summary: Sign up a new user + /trips: + post: + consumes: + - application/json + description: Create a new trip with given fields + parameters: + - description: Trip details + in: body + name: trip + required: true + schema: + $ref: '#/definitions/models.Trip' + produces: + - application/json + responses: + "201": + description: Trip created successfully + schema: + $ref: '#/definitions/models.Trip' + "400": + description: Invalid request + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "404": + description: Invalid request + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "500": + description: Failed to create trip + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + summary: Create a new trip + /trips/{id}: + delete: + description: Delete a trip by trip ID + parameters: + - description: Trip ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "204": + description: Trip deleted successfully + "400": + description: Invalid trip ID + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "404": + description: Trip not found + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "500": + description: Failed to delete trip + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + summary: Delete a trip + get: + description: Get trip details by trip ID + parameters: + - description: Trip ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Trip details + schema: + $ref: '#/definitions/models.Trip' + "400": + description: Invalid trip ID + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "404": + description: Trip not found + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "500": + description: Failed to retrieve trip + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + summary: Retrieve a trip by ID + put: + consumes: + - application/json + description: Update trip details by trip ID + parameters: + - description: Trip ID + in: path + name: id + required: true + type: integer + - description: Updated trip details + in: body + name: trip + required: true + schema: + $ref: '#/definitions/models.Trip' + produces: + - application/json + responses: + "200": + description: Trip updated successfully + schema: + $ref: '#/definitions/models.Trip' + "400": + description: Invalid trip data + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "404": + description: Trip not found + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "500": + description: Failed to update trip + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + summary: Update an existing trip + /users/{userID}/trips: + get: + description: Get all trips for a specific user + parameters: + - description: User ID + in: path + name: userID + required: true + type: integer + produces: + - application/json + responses: + "200": + description: List of trips + schema: + items: + $ref: '#/definitions/models.Trip' + type: array + "400": + description: Invalid user ID + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "404": + description: Trips not found + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + "500": + description: Failed to retrieve trips + schema: + $ref: '#/definitions/httpresponses.ErrorResponse' + summary: Retrieve trips by user ID /users/me: get: description: Retrieve the current authenticated user information diff --git a/internal/models/city.go b/internal/models/city.go new file mode 100644 index 0000000..f40ceaa --- /dev/null +++ b/internal/models/city.go @@ -0,0 +1,9 @@ +package models + +import "time" + +type City struct { + ID uint `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/models/error.go b/internal/models/error.go index 2ef7fdd..2e23081 100644 --- a/internal/models/error.go +++ b/internal/models/error.go @@ -4,4 +4,6 @@ import "errors" var ( ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") + ErrInternal = errors.New("internal repository error") ) diff --git a/internal/models/trip.go b/internal/models/trip.go new file mode 100644 index 0000000..6792696 --- /dev/null +++ b/internal/models/trip.go @@ -0,0 +1,15 @@ +package models + +import "time" + +type Trip struct { + ID uint `json:"id"` + UserID uint `json:"user_id"` + Name string `json:"name"` + Description string `json:"description"` + CityID uint `json:"city_id"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + Private bool `json:"private"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/internal/pkg/trips/delivery/http/handler.go b/internal/pkg/trips/delivery/http/handler.go new file mode 100644 index 0000000..069fe9d --- /dev/null +++ b/internal/pkg/trips/delivery/http/handler.go @@ -0,0 +1,232 @@ +package http + +import ( + "2024_2_ThereWillBeName/internal/models" + httpresponse "2024_2_ThereWillBeName/internal/pkg/httpresponses" + "2024_2_ThereWillBeName/internal/pkg/trips" + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +type TripHandler struct { + uc trips.TripsUsecase +} + +func NewTripHandler(uc trips.TripsUsecase) *TripHandler { + return &TripHandler{uc} +} + +func ErrorCheck(err error, action string) (httpresponse.ErrorResponse, int) { + if errors.Is(err, models.ErrNotFound) { + log.Printf("%s error: %s", action, err) + response := httpresponse.ErrorResponse{ + Message: "Invalid request", + } + return response, http.StatusNotFound + } + log.Printf("%s error: %s", action, err) + response := httpresponse.ErrorResponse{ + Message: fmt.Sprintf("Failed to %s trip", action), + } + return response, http.StatusInternalServerError +} + +// CreateTripHandler godoc +// @Summary Create a new trip +// @Description Create a new trip with given fields +// @Accept json +// @Produce json +// @Param trip body models.Trip true "Trip details" +// @Success 201 {object} models.Trip "Trip created successfully" +// @Failure 400 {object} httpresponses.ErrorResponse "Invalid request" +// @Failure 404 {object} httpresponses.ErrorResponse "Invalid request" +// @Failure 500 {object} httpresponses.ErrorResponse "Failed to create trip" +// @Router /trips [post] +func (h *TripHandler) CreateTripHandler(w http.ResponseWriter, r *http.Request) { + var trip models.Trip + err := json.NewDecoder(r.Body).Decode(&trip) + if err != nil { + log.Printf("create error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid request", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + + err = h.uc.CreateTrip(context.Background(), trip) + if err != nil { + response, status := ErrorCheck(err, "create") + httpresponse.SendJSONResponse(w, response, status) + return + } + + w.WriteHeader(http.StatusCreated) +} + +// UpdateTripHandler godoc +// @Summary Update an existing trip +// @Description Update trip details by trip ID +// @Accept json +// @Produce json +// @Param id path int true "Trip ID" +// @Param trip body models.Trip true "Updated trip details" +// @Success 200 {object} models.Trip "Trip updated successfully" +// @Failure 400 {object} httpresponses.ErrorResponse "Invalid trip ID" +// @Failure 400 {object} httpresponses.ErrorResponse "Invalid trip data" +// @Failure 404 {object} httpresponses.ErrorResponse "Trip not found" +// @Failure 500 {object} httpresponses.ErrorResponse "Failed to update trip" +// @Router /trips/{id} [put] +func (h *TripHandler) UpdateTripHandler(w http.ResponseWriter, r *http.Request) { + var trip models.Trip + vars := mux.Vars(r) + tripID, err := strconv.Atoi(vars["id"]) + if err != nil || tripID < 0 { + log.Printf("update error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid trip ID", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + err = json.NewDecoder(r.Body).Decode(&trip) + if err != nil { + log.Printf("update error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid trip data", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + + trip.ID = uint(tripID) + err = h.uc.UpdateTrip(context.Background(), trip) + if err != nil { + response, status := ErrorCheck(err, "update") + httpresponse.SendJSONResponse(w, response, status) + return + } + + w.WriteHeader(http.StatusOK) +} + +// DeleteTripHandler godoc +// @Summary Delete a trip +// @Description Delete a trip by trip ID +// @Produce json +// @Param id path int true "Trip ID" +// @Success 204 "Trip deleted successfully" +// @Failure 400 {object} httpresponses.ErrorResponse "Invalid trip ID" +// @Failure 404 {object} httpresponses.ErrorResponse "Trip not found" +// @Failure 500 {object} httpresponses.ErrorResponse "Failed to delete trip" +// @Router /trips/{id} [delete] +func (h *TripHandler) DeleteTripHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + idStr := vars["id"] + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + log.Printf("delete error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid trip ID", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + + err = h.uc.DeleteTrip(context.Background(), uint(id)) + if err != nil { + response, status := ErrorCheck(err, "delete") + httpresponse.SendJSONResponse(w, response, status) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// GetTripsByUserIDHandler godoc +// @Summary Retrieve trips by user ID +// @Description Get all trips for a specific user +// @Produce json +// @Param userID path int true "User ID" +// @Success 200 {array} models.Trip "List of trips" +// @Failure 400 {object} httpresponses.ErrorResponse "Invalid user ID" +// @Failure 404 {object} httpresponses.ErrorResponse "Invalid user ID" +// @Failure 404 {object} httpresponses.ErrorResponse "Trips not found" +// @Failure 500 {object} httpresponses.ErrorResponse "Failed to retrieve trips" +// @Router /users/{userID}/trips [get] +func (h *TripHandler) GetTripsByUserIDHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + userIDStr := vars["userID"] + userID, err := strconv.ParseUint(userIDStr, 10, 64) + if err != nil { + log.Printf("retrieve error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid user ID", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + pageStr := r.URL.Query().Get("page") + page := 1 + if pageStr != "" { + page, err = strconv.Atoi(pageStr) + if err != nil { + log.Printf("retrieve error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid page number", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + } + limit := 10 + offset := limit * (page - 1) + trip, err := h.uc.GetTripsByUserID(context.Background(), uint(userID), limit, offset) + if err != nil { + response, status := ErrorCheck(err, "retrieve") + httpresponse.SendJSONResponse(w, response, status) + return + } + + httpresponse.SendJSONResponse(w, trip, http.StatusOK) +} + +// GetTripHandler godoc +// @Summary Retrieve a trip by ID +// @Description Get trip details by trip ID +// @Produce json +// @Param id path int true "Trip ID" +// @Success 200 {object} models.Trip "Trip details" +// @Failure 400 {object} httpresponses.ErrorResponse "Invalid trip ID" +// @Failure 404 {object} httpresponses.ErrorResponse "Trip not found" +// @Failure 500 {object} httpresponses.ErrorResponse "Failed to retrieve trip" +// @Router /trips/{id} [get] +func (h *TripHandler) GetTripHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + tripIDStr := vars["id"] + tripID, err := strconv.ParseUint(tripIDStr, 10, 64) + if err != nil { + log.Printf("retrieve error: %s", err) + response := httpresponse.ErrorResponse{ + Message: "Invalid trip ID", + } + httpresponse.SendJSONResponse(w, response, http.StatusBadRequest) + return + } + + trip, err := h.uc.GetTrip(context.Background(), uint(tripID)) + if err != nil { + response, status := ErrorCheck(err, "retrieve") + httpresponse.SendJSONResponse(w, response, status) + return + } + + httpresponse.SendJSONResponse(w, trip, http.StatusOK) +} diff --git a/internal/pkg/trips/interfaces.go b/internal/pkg/trips/interfaces.go new file mode 100644 index 0000000..3bae38d --- /dev/null +++ b/internal/pkg/trips/interfaces.go @@ -0,0 +1,22 @@ +package trips + +import ( + "2024_2_ThereWillBeName/internal/models" + "context" +) + +type TripsUsecase interface { + CreateTrip(ctx context.Context, trip models.Trip) error + UpdateTrip(ctx context.Context, user models.Trip) error + DeleteTrip(ctx context.Context, id uint) error + GetTripsByUserID(ctx context.Context, userID uint, limit, offset int) ([]models.Trip, error) + GetTrip(ctx context.Context, tripID uint) (models.Trip, error) +} + +type TripsRepo interface { + CreateTrip(ctx context.Context, user models.Trip) error + UpdateTrip(ctx context.Context, user models.Trip) error + DeleteTrip(ctx context.Context, id uint) error + GetTripsByUserID(ctx context.Context, userID uint, limit, offset int) ([]models.Trip, error) + GetTrip(ctx context.Context, tripID uint) (models.Trip, error) +} diff --git a/internal/pkg/trips/repo/trips_repository.go b/internal/pkg/trips/repo/trips_repository.go new file mode 100644 index 0000000..f035bd9 --- /dev/null +++ b/internal/pkg/trips/repo/trips_repository.go @@ -0,0 +1,124 @@ +package repo + +import ( + "2024_2_ThereWillBeName/internal/models" + + "context" + "database/sql" + "errors" + "fmt" +) + +type TripRepository struct { + db *sql.DB +} + +func NewTripRepository(db *sql.DB) *TripRepository { + return &TripRepository{db: db} +} + +func (r *TripRepository) CreateTrip(ctx context.Context, trip models.Trip) error { + query := `INSERT INTO trips (user_id, name, description, city_id, start_date, end_date, private, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())` + + result, err := r.db.ExecContext(ctx, query, trip.UserID, trip.Name, trip.Description, trip.CityID, trip.StartDate, trip.EndDate, trip.Private) + if err != nil { + return fmt.Errorf("failed to create a trip: %w", models.ErrInternal) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to retrieve rows affected: %w", models.ErrInternal) + } + if rowsAffected == 0 { + return fmt.Errorf("no rows were created: %w", models.ErrNotFound) + } + + return nil +} + +func (r *TripRepository) UpdateTrip(ctx context.Context, trip models.Trip) error { + query := `UPDATE trips + SET name = $1, description = $2, city_id = $3, start_date = $4, end_date = $5, private = $6 + WHERE id = $7` + + result, err := r.db.ExecContext(ctx, query, trip.Name, trip.Description, trip.CityID, trip.StartDate, trip.EndDate, trip.Private, trip.ID) + if err != nil { + return fmt.Errorf("failed to execute update query: %w", models.ErrInternal) + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to retrieve rows affected: %w", models.ErrInternal) + } + if rowsAffected == 0 { + return fmt.Errorf("no rows were updated: %w", models.ErrNotFound) + } + + return nil +} + +func (r *TripRepository) DeleteTrip(ctx context.Context, id uint) error { + query := `DELETE FROM trips WHERE id = $1` + result, err := r.db.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete trip: %w", models.ErrInternal) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to retrieve rows affected %w", models.ErrInternal) + } + if rowsAffected == 0 { + return fmt.Errorf("no rows were deleted: %w", models.ErrNotFound) + } + return nil +} + +func (r *TripRepository) GetTripsByUserID(ctx context.Context, userID uint, limit, offset int) ([]models.Trip, error) { + query := `SELECT id, user_id, name, description, city_id, start_date, end_date, private, created_at + FROM trips + WHERE user_id = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3` + + rows, err := r.db.QueryContext(ctx, query, userID, limit, offset) + + if err != nil { + return nil, fmt.Errorf("failed to retrieve trips: %w", models.ErrInternal) + } + defer rows.Close() + + var tripRows []models.Trip + for rows.Next() { + var trip models.Trip + if err := rows.Scan(&trip.ID, &trip.UserID, &trip.Name, &trip.Description, &trip.CityID, &trip.StartDate, &trip.EndDate, &trip.Private, &trip.CreatedAt); err != nil { + return nil, fmt.Errorf("failed to scan trip row: %w", models.ErrInternal) + } + tripRows = append(tripRows, trip) + } + + if len(tripRows) == 0 { + return nil, fmt.Errorf("no trips found: %w", models.ErrNotFound) + } + + return tripRows, nil +} + +func (r *TripRepository) GetTrip(ctx context.Context, tripID uint) (models.Trip, error) { + query := `SELECT id, user_id, name, description, city_id, start_date, end_date, private, created_at + FROM trips + WHERE id = $1` + + row := r.db.QueryRowContext(ctx, query, tripID) + + var trip models.Trip + err := row.Scan(&trip.ID, &trip.UserID, &trip.Name, &trip.Description, &trip.CityID, &trip.StartDate, &trip.EndDate, &trip.Private, &trip.CreatedAt) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return models.Trip{}, fmt.Errorf("trip not found: %w", models.ErrNotFound) + } + return models.Trip{}, fmt.Errorf("failed to scan trip row: %w", models.ErrInternal) + } + + return trip, nil +} diff --git a/internal/pkg/trips/usecase/trips_usecase.go b/internal/pkg/trips/usecase/trips_usecase.go new file mode 100644 index 0000000..db0fde7 --- /dev/null +++ b/internal/pkg/trips/usecase/trips_usecase.go @@ -0,0 +1,79 @@ +package usecase + +import ( + "2024_2_ThereWillBeName/internal/models" + "2024_2_ThereWillBeName/internal/pkg/trips" + "context" + "errors" + "fmt" +) + +type TripsUsecaseImpl struct { + tripRepo trips.TripsRepo +} + +func NewTripsUsecase(repo trips.TripsRepo) *TripsUsecaseImpl { + return &TripsUsecaseImpl{ + tripRepo: repo, + } +} + +func (u *TripsUsecaseImpl) CreateTrip(ctx context.Context, trip models.Trip) error { + err := u.tripRepo.CreateTrip(ctx, trip) + if err != nil { + if errors.Is(err, models.ErrNotFound) { + return fmt.Errorf("invalid request: %w", models.ErrNotFound) + } else { + return fmt.Errorf("internal error: %w", models.ErrInternal) + } + } + + return nil +} + +func (u *TripsUsecaseImpl) UpdateTrip(ctx context.Context, trip models.Trip) error { + err := u.tripRepo.UpdateTrip(ctx, trip) + if err != nil { + if errors.Is(err, models.ErrNotFound) { + return fmt.Errorf("invalid request: %w", models.ErrNotFound) + } else { + return fmt.Errorf("internal error: %w", models.ErrInternal) + } + } + + return nil +} + +func (u *TripsUsecaseImpl) DeleteTrip(ctx context.Context, id uint) error { + err := u.tripRepo.DeleteTrip(ctx, id) + if err != nil { + if errors.Is(err, models.ErrNotFound) { + return fmt.Errorf("invalid request: %w", models.ErrNotFound) + } + return fmt.Errorf("internal error: %w", models.ErrInternal) + } + + return nil +} + +func (u *TripsUsecaseImpl) GetTripsByUserID(ctx context.Context, userID uint, limit, offset int) ([]models.Trip, error) { + tripsFound, err := u.tripRepo.GetTripsByUserID(ctx, userID, limit, offset) + if err != nil { + if errors.Is(err, models.ErrNotFound) { + return nil, fmt.Errorf("invalid request: %w", models.ErrNotFound) + } + return nil, fmt.Errorf("internal error: %w", models.ErrInternal) + } + return tripsFound, nil +} + +func (u *TripsUsecaseImpl) GetTrip(ctx context.Context, tripID uint) (models.Trip, error) { + trip, err := u.tripRepo.GetTrip(ctx, tripID) + if err != nil { + if errors.Is(err, models.ErrNotFound) { + return models.Trip{}, fmt.Errorf("invalid request: %w", models.ErrNotFound) + } + return models.Trip{}, fmt.Errorf("internal error^ %w", models.ErrInternal) + } + return trip, nil +} diff --git a/migrations/01_up.sql b/migrations/01_up.sql index f47ff7f..f954029 100644 --- a/migrations/01_up.sql +++ b/migrations/01_up.sql @@ -5,3 +5,42 @@ CREATE TABLE IF NOT EXISTS users password VARCHAR(255) NOT NULL, -- Хэш пароля created_at TIMESTAMP NOT NULL DEFAULT NOW() -- Дата создания пользователя ); + +CREATE TABLE IF NOT EXISTS cities +( + id SERIAL PRIMARY KEY, -- Уникальный идентификатор города + name VARCHAR(255) NOT NULL, -- Название города + created_at TIMESTAMP NOT NULL DEFAULT NOW() -- Дата создания города +); + +CREATE TABLE IF NOT EXISTS trips +( + id SERIAL PRIMARY KEY, -- Уникальный идентификатор поездки + name VARCHAR(255) NOT NULL, -- Название поездки + description VARCHAR(1000), -- Описание поездки + city_id INTEGER NOT NULL, -- Направление поездки + start_date DATE, -- Дата начала поездки + end_date DATE, -- Дата окончания поездки + private BOOLEAN DEFAULT TRUE, -- Кому видна поездка (всем или выбранным пользователям) + created_at TIMESTAMP NOT NULL DEFAULT NOW(), -- Дата создания поездки + user_id INTEGER NOT NULL, -- Идентификатор пользователя-создателя поездки + CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT fk_city FOREIGN KEY (city_id) REFERENCES cities(id) ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS places +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, -- название места + image VARCHAR(255) NOT NULL, -- путь к картинке + description TEXT NOT NULL -- описание места +); + +CREATE TABLE IF NOT EXISTS trips_places ( --таблица для сопоставления поездки и достопримечательности, которая в нее входит + id SERIAL PRIMARY KEY, + trip_id INT NOT NULL, -- Идентификатор поездки + place_id INT NOT NULL, -- Идентификатор города + created_at TIMESTAMP NOT NULL DEFAULT NOW(), -- Дата создания записи + FOREIGN KEY (trip_id) REFERENCES trips(id) ON DELETE CASCADE, + FOREIGN KEY (place_id) REFERENCES places(id) ON DELETE CASCADE +); \ No newline at end of file