From a3f00cd8ffb8242f8db991e7371ddfdfaa9f0211 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 20 Dec 2025 08:06:07 -0300 Subject: [PATCH] refactor(handler): extract order and cart handlers - Extract 5 order handlers to order_handler.go (147 lines) - CreateOrder, ListOrders, GetOrder, UpdateOrderStatus, DeleteOrder - Extract 4 cart/review handlers to cart_handler.go (127 lines) - CreateReview, AddToCart, GetCart, DeleteCartItem - handler.go reduced from 806 to 548 lines - Total refactoring: ~63% of original (1471 -> 548) All tests passing --- backend/internal/http/handler/cart_handler.go | 128 +++++++++ backend/internal/http/handler/handler.go | 258 ------------------ .../internal/http/handler/order_handler.go | 145 ++++++++++ 3 files changed, 273 insertions(+), 258 deletions(-) create mode 100644 backend/internal/http/handler/cart_handler.go create mode 100644 backend/internal/http/handler/order_handler.go diff --git a/backend/internal/http/handler/cart_handler.go b/backend/internal/http/handler/cart_handler.go new file mode 100644 index 0000000..d7d0fff --- /dev/null +++ b/backend/internal/http/handler/cart_handler.go @@ -0,0 +1,128 @@ +package handler + +import ( + "errors" + "net/http" + + "github.com/saveinmed/backend-go/internal/http/middleware" +) + +// CreateReview godoc +// @Summary Criar avaliação +// @Tags Avaliações +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param payload body createReviewRequest true "Dados da avaliação" +// @Success 201 {object} domain.Review +// @Failure 400 {object} map[string]string +// @Router /api/v1/reviews [post] +// CreateReview allows buyers to rate the seller after delivery. +func (h *Handler) CreateReview(w http.ResponseWriter, r *http.Request) { + claims, ok := middleware.GetClaims(r.Context()) + if !ok || claims.CompanyID == nil { + writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) + return + } + + var req createReviewRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + review, err := h.svc.CreateReview(r.Context(), *claims.CompanyID, req.OrderID, req.Rating, req.Comment) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + writeJSON(w, http.StatusCreated, review) +} + +// AddToCart godoc +// @Summary Adicionar item ao carrinho +// @Tags Carrinho +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param payload body addCartItemRequest true "Item do carrinho" +// @Success 201 {object} domain.CartSummary +// @Failure 400 {object} map[string]string +// @Router /api/v1/cart [post] +// AddToCart appends an item to the authenticated buyer cart respecting stock. +func (h *Handler) AddToCart(w http.ResponseWriter, r *http.Request) { + claims, ok := middleware.GetClaims(r.Context()) + if !ok || claims.CompanyID == nil { + writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) + return + } + + var req addCartItemRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + summary, err := h.svc.AddItemToCart(r.Context(), *claims.CompanyID, req.ProductID, req.Quantity) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + writeJSON(w, http.StatusCreated, summary) +} + +// GetCart godoc +// @Summary Obter carrinho +// @Tags Carrinho +// @Security BearerAuth +// @Produce json +// @Success 200 {object} domain.CartSummary +// @Router /api/v1/cart [get] +// GetCart returns cart contents and totals for the authenticated buyer. +func (h *Handler) GetCart(w http.ResponseWriter, r *http.Request) { + claims, ok := middleware.GetClaims(r.Context()) + if !ok || claims.CompanyID == nil { + writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) + return + } + + summary, err := h.svc.ListCart(r.Context(), *claims.CompanyID) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, summary) +} + +// DeleteCartItem godoc +// @Summary Remover item do carrinho +// @Tags Carrinho +// @Security BearerAuth +// @Param id path string true "Cart item ID" +// @Success 200 {object} domain.CartSummary +// @Failure 400 {object} map[string]string +// @Router /api/v1/cart/{id} [delete] +// DeleteCartItem removes a product from the cart and returns the updated totals. +func (h *Handler) DeleteCartItem(w http.ResponseWriter, r *http.Request) { + claims, ok := middleware.GetClaims(r.Context()) + if !ok || claims.CompanyID == nil { + writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) + return + } + + id, err := parseUUIDFromPath(r.URL.Path) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + summary, err := h.svc.RemoveCartItem(r.Context(), *claims.CompanyID, id) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + writeJSON(w, http.StatusOK, summary) +} diff --git a/backend/internal/http/handler/handler.go b/backend/internal/http/handler/handler.go index 7f56735..66ee401 100644 --- a/backend/internal/http/handler/handler.go +++ b/backend/internal/http/handler/handler.go @@ -10,7 +10,6 @@ import ( "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" - "github.com/saveinmed/backend-go/internal/http/middleware" "github.com/saveinmed/backend-go/internal/usecase" ) @@ -111,263 +110,6 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp}) } -// CreateOrder godoc -// @Summary Criação de pedido com split -// @Tags Pedidos -// @Accept json -// @Produce json -// @Param order body createOrderRequest true "Pedido" -// @Success 201 {object} domain.Order -// @Router /api/v1/orders [post] -func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) { - var req createOrderRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - order := &domain.Order{ - BuyerID: req.BuyerID, - SellerID: req.SellerID, - Items: req.Items, - Shipping: req.Shipping, - } - - var total int64 - for _, item := range req.Items { - total += item.UnitCents * item.Quantity - } - order.TotalCents = total - - if err := h.svc.CreateOrder(r.Context(), order); err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - - writeJSON(w, http.StatusCreated, order) -} - -// ListOrders godoc -// @Summary Listar pedidos -// @Tags Pedidos -// @Security BearerAuth -// @Produce json -// @Success 200 {array} domain.Order -// @Router /api/v1/orders [get] -func (h *Handler) ListOrders(w http.ResponseWriter, r *http.Request) { - orders, err := h.svc.ListOrders(r.Context()) - if err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - - writeJSON(w, http.StatusOK, orders) -} - -// GetOrder godoc -// @Summary Consulta pedido -// @Tags Pedidos -// @Security BearerAuth -// @Produce json -// @Param id path string true "Order ID" -// @Success 200 {object} domain.Order -// @Router /api/v1/orders/{id} [get] -func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { - id, err := parseUUIDFromPath(r.URL.Path) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - order, err := h.svc.GetOrder(r.Context(), id) - if err != nil { - writeError(w, http.StatusNotFound, err) - return - } - - writeJSON(w, http.StatusOK, order) -} - -// UpdateOrderStatus godoc -// @Summary Atualiza status do pedido -// @Tags Pedidos -// @Security BearerAuth -// @Accept json -// @Produce json -// @Param id path string true "Order ID" -// @Param status body updateStatusRequest true "Novo status" -// @Success 204 "" -// @Router /api/v1/orders/{id}/status [patch] -func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) { - id, err := parseUUIDFromPath(r.URL.Path) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - var req updateStatusRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - if !isValidStatus(req.Status) { - writeError(w, http.StatusBadRequest, errors.New("invalid status")) - return - } - - if err := h.svc.UpdateOrderStatus(r.Context(), id, domain.OrderStatus(req.Status)); err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -// DeleteOrder godoc -// @Summary Remover pedido -// @Tags Pedidos -// @Security BearerAuth -// @Param id path string true "Order ID" -// @Success 204 "" -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Router /api/v1/orders/{id} [delete] -func (h *Handler) DeleteOrder(w http.ResponseWriter, r *http.Request) { - id, err := parseUUIDFromPath(r.URL.Path) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - if err := h.svc.DeleteOrder(r.Context(), id); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -// CreateReview godoc -// @Summary Criar avaliação -// @Tags Avaliações -// @Security BearerAuth -// @Accept json -// @Produce json -// @Param payload body createReviewRequest true "Dados da avaliação" -// @Success 201 {object} domain.Review -// @Failure 400 {object} map[string]string -// @Router /api/v1/reviews [post] -// CreateReview allows buyers to rate the seller after delivery. -func (h *Handler) CreateReview(w http.ResponseWriter, r *http.Request) { - claims, ok := middleware.GetClaims(r.Context()) - if !ok || claims.CompanyID == nil { - writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) - return - } - - var req createReviewRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - review, err := h.svc.CreateReview(r.Context(), *claims.CompanyID, req.OrderID, req.Rating, req.Comment) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - writeJSON(w, http.StatusCreated, review) -} - -// AddToCart godoc -// @Summary Adicionar item ao carrinho -// @Tags Carrinho -// @Security BearerAuth -// @Accept json -// @Produce json -// @Param payload body addCartItemRequest true "Item do carrinho" -// @Success 201 {object} domain.CartSummary -// @Failure 400 {object} map[string]string -// @Router /api/v1/cart [post] -// AddToCart appends an item to the authenticated buyer cart respecting stock. -func (h *Handler) AddToCart(w http.ResponseWriter, r *http.Request) { - claims, ok := middleware.GetClaims(r.Context()) - if !ok || claims.CompanyID == nil { - writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) - return - } - - var req addCartItemRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - summary, err := h.svc.AddItemToCart(r.Context(), *claims.CompanyID, req.ProductID, req.Quantity) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - writeJSON(w, http.StatusCreated, summary) -} - -// GetCart godoc -// @Summary Obter carrinho -// @Tags Carrinho -// @Security BearerAuth -// @Produce json -// @Success 200 {object} domain.CartSummary -// @Router /api/v1/cart [get] -// GetCart returns cart contents and totals for the authenticated buyer. -func (h *Handler) GetCart(w http.ResponseWriter, r *http.Request) { - claims, ok := middleware.GetClaims(r.Context()) - if !ok || claims.CompanyID == nil { - writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) - return - } - - summary, err := h.svc.ListCart(r.Context(), *claims.CompanyID) - if err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - writeJSON(w, http.StatusOK, summary) -} - -// DeleteCartItem godoc -// @Summary Remover item do carrinho -// @Tags Carrinho -// @Security BearerAuth -// @Param id path string true "Cart item ID" -// @Success 200 {object} domain.CartSummary -// @Failure 400 {object} map[string]string -// @Router /api/v1/cart/{id} [delete] -// DeleteCartItem removes a product from the cart and returns the updated totals. -func (h *Handler) DeleteCartItem(w http.ResponseWriter, r *http.Request) { - claims, ok := middleware.GetClaims(r.Context()) - if !ok || claims.CompanyID == nil { - writeError(w, http.StatusBadRequest, errors.New("missing buyer context")) - return - } - - id, err := parseUUIDFromPath(r.URL.Path) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - summary, err := h.svc.RemoveCartItem(r.Context(), *claims.CompanyID, id) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - writeJSON(w, http.StatusOK, summary) -} - // CreatePaymentPreference godoc // @Summary Cria preferência de pagamento Mercado Pago com split nativo // @Tags Pagamentos diff --git a/backend/internal/http/handler/order_handler.go b/backend/internal/http/handler/order_handler.go new file mode 100644 index 0000000..ba080dd --- /dev/null +++ b/backend/internal/http/handler/order_handler.go @@ -0,0 +1,145 @@ +package handler + +import ( + "errors" + "net/http" + + "github.com/saveinmed/backend-go/internal/domain" +) + +// CreateOrder godoc +// @Summary Criação de pedido com split +// @Tags Pedidos +// @Accept json +// @Produce json +// @Param order body createOrderRequest true "Pedido" +// @Success 201 {object} domain.Order +// @Router /api/v1/orders [post] +func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) { + var req createOrderRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + order := &domain.Order{ + BuyerID: req.BuyerID, + SellerID: req.SellerID, + Items: req.Items, + Shipping: req.Shipping, + } + + var total int64 + for _, item := range req.Items { + total += item.UnitCents * item.Quantity + } + order.TotalCents = total + + if err := h.svc.CreateOrder(r.Context(), order); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusCreated, order) +} + +// ListOrders godoc +// @Summary Listar pedidos +// @Tags Pedidos +// @Security BearerAuth +// @Produce json +// @Success 200 {array} domain.Order +// @Router /api/v1/orders [get] +func (h *Handler) ListOrders(w http.ResponseWriter, r *http.Request) { + orders, err := h.svc.ListOrders(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusOK, orders) +} + +// GetOrder godoc +// @Summary Consulta pedido +// @Tags Pedidos +// @Security BearerAuth +// @Produce json +// @Param id path string true "Order ID" +// @Success 200 {object} domain.Order +// @Router /api/v1/orders/{id} [get] +func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { + id, err := parseUUIDFromPath(r.URL.Path) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + order, err := h.svc.GetOrder(r.Context(), id) + if err != nil { + writeError(w, http.StatusNotFound, err) + return + } + + writeJSON(w, http.StatusOK, order) +} + +// UpdateOrderStatus godoc +// @Summary Atualiza status do pedido +// @Tags Pedidos +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param id path string true "Order ID" +// @Param status body updateStatusRequest true "Novo status" +// @Success 204 "" +// @Router /api/v1/orders/{id}/status [patch] +func (h *Handler) UpdateOrderStatus(w http.ResponseWriter, r *http.Request) { + id, err := parseUUIDFromPath(r.URL.Path) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + var req updateStatusRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + if !isValidStatus(req.Status) { + writeError(w, http.StatusBadRequest, errors.New("invalid status")) + return + } + + if err := h.svc.UpdateOrderStatus(r.Context(), id, domain.OrderStatus(req.Status)); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// DeleteOrder godoc +// @Summary Remover pedido +// @Tags Pedidos +// @Security BearerAuth +// @Param id path string true "Order ID" +// @Success 204 "" +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/v1/orders/{id} [delete] +func (h *Handler) DeleteOrder(w http.ResponseWriter, r *http.Request) { + id, err := parseUUIDFromPath(r.URL.Path) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + if err := h.svc.DeleteOrder(r.Context(), id); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + w.WriteHeader(http.StatusNoContent) +}