From 19c636164bea6056d0ddeb86b389354922384110 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 20 Dec 2025 08:02:02 -0300 Subject: [PATCH] refactor(handler): extract product and inventory handlers - Extract 7 handlers to product_handler.go (216 lines) - CreateProduct, ListProducts, GetProduct, UpdateProduct, DeleteProduct - ListInventory, AdjustInventory - handler.go reduced from 1025 to 806 lines - Total refactoring: ~60% of original (1471 -> 806) All tests passing --- backend/internal/http/handler/handler.go | 219 ----------------- .../internal/http/handler/product_handler.go | 227 ++++++++++++++++++ 2 files changed, 227 insertions(+), 219 deletions(-) create mode 100644 backend/internal/http/handler/product_handler.go diff --git a/backend/internal/http/handler/handler.go b/backend/internal/http/handler/handler.go index 01b0960..7f56735 100644 --- a/backend/internal/http/handler/handler.go +++ b/backend/internal/http/handler/handler.go @@ -3,9 +3,7 @@ package handler import ( "errors" "net/http" - "strconv" "strings" - "time" jsoniter "github.com/json-iterator/go" @@ -113,223 +111,6 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp}) } -// CreateProduct godoc -// @Summary Cadastro de produto com rastreabilidade de lote -// @Tags Produtos -// @Accept json -// @Produce json -// @Param product body registerProductRequest true "Produto" -// @Success 201 {object} domain.Product -// @Router /api/v1/products [post] -func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) { - var req registerProductRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - product := &domain.Product{ - SellerID: req.SellerID, - Name: req.Name, - Description: req.Description, - Batch: req.Batch, - ExpiresAt: req.ExpiresAt, - PriceCents: req.PriceCents, - Stock: req.Stock, - } - - if err := h.svc.RegisterProduct(r.Context(), product); err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - - writeJSON(w, http.StatusCreated, product) -} - -// ListProducts godoc -// @Summary Lista catálogo com lote e validade -// @Tags Produtos -// @Produce json -// @Success 200 {array} domain.Product -// @Router /api/v1/products [get] -func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) { - products, err := h.svc.ListProducts(r.Context()) - if err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - writeJSON(w, http.StatusOK, products) -} - -// GetProduct godoc -// @Summary Obter produto -// @Tags Produtos -// @Produce json -// @Param id path string true "Product ID" -// @Success 200 {object} domain.Product -// @Failure 404 {object} map[string]string -// @Router /api/v1/products/{id} [get] -func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) { - id, err := parseUUIDFromPath(r.URL.Path) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - product, err := h.svc.GetProduct(r.Context(), id) - if err != nil { - writeError(w, http.StatusNotFound, err) - return - } - - writeJSON(w, http.StatusOK, product) -} - -// UpdateProduct godoc -// @Summary Atualizar produto -// @Tags Produtos -// @Accept json -// @Produce json -// @Param id path string true "Product ID" -// @Param payload body updateProductRequest true "Campos para atualização" -// @Success 200 {object} domain.Product -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Router /api/v1/products/{id} [patch] -func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) { - id, err := parseUUIDFromPath(r.URL.Path) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - var req updateProductRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - product, err := h.svc.GetProduct(r.Context(), id) - if err != nil { - writeError(w, http.StatusNotFound, err) - return - } - - if req.SellerID != nil { - product.SellerID = *req.SellerID - } - if req.Name != nil { - product.Name = *req.Name - } - if req.Description != nil { - product.Description = *req.Description - } - if req.Batch != nil { - product.Batch = *req.Batch - } - if req.ExpiresAt != nil { - product.ExpiresAt = *req.ExpiresAt - } - if req.PriceCents != nil { - product.PriceCents = *req.PriceCents - } - if req.Stock != nil { - product.Stock = *req.Stock - } - - if err := h.svc.UpdateProduct(r.Context(), product); err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - - writeJSON(w, http.StatusOK, product) -} - -// DeleteProduct godoc -// @Summary Remover produto -// @Tags Produtos -// @Param id path string true "Product ID" -// @Success 204 "" -// @Failure 400 {object} map[string]string -// @Failure 404 {object} map[string]string -// @Router /api/v1/products/{id} [delete] -func (h *Handler) DeleteProduct(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.DeleteProduct(r.Context(), id); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -// ListInventory godoc -// @Summary Listar estoque -// @Tags Estoque -// @Security BearerAuth -// @Produce json -// @Param expires_in_days query int false "Dias para expiração" -// @Success 200 {array} domain.InventoryItem -// @Router /api/v1/inventory [get] -// ListInventory exposes stock with expiring batch filters. -func (h *Handler) ListInventory(w http.ResponseWriter, r *http.Request) { - var filter domain.InventoryFilter - if days := r.URL.Query().Get("expires_in_days"); days != "" { - n, err := strconv.Atoi(days) - if err != nil || n < 0 { - writeError(w, http.StatusBadRequest, errors.New("invalid expires_in_days")) - return - } - expires := time.Now().Add(time.Duration(n) * 24 * time.Hour) - filter.ExpiringBefore = &expires - } - - inventory, err := h.svc.ListInventory(r.Context(), filter) - if err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - - writeJSON(w, http.StatusOK, inventory) -} - -// AdjustInventory godoc -// @Summary Ajustar estoque -// @Tags Estoque -// @Security BearerAuth -// @Accept json -// @Produce json -// @Param payload body inventoryAdjustRequest true "Ajuste de estoque" -// @Success 200 {object} domain.InventoryItem -// @Failure 400 {object} map[string]string -// @Router /api/v1/inventory/adjust [post] -// AdjustInventory handles manual stock corrections. -func (h *Handler) AdjustInventory(w http.ResponseWriter, r *http.Request) { - var req inventoryAdjustRequest - if err := decodeJSON(r.Context(), r, &req); err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - if req.Delta == 0 { - writeError(w, http.StatusBadRequest, errors.New("delta must be non-zero")) - return - } - - item, err := h.svc.AdjustInventory(r.Context(), req.ProductID, req.Delta, req.Reason) - if err != nil { - writeError(w, http.StatusBadRequest, err) - return - } - - writeJSON(w, http.StatusOK, item) -} - // CreateOrder godoc // @Summary Criação de pedido com split // @Tags Pedidos diff --git a/backend/internal/http/handler/product_handler.go b/backend/internal/http/handler/product_handler.go new file mode 100644 index 0000000..a6b16db --- /dev/null +++ b/backend/internal/http/handler/product_handler.go @@ -0,0 +1,227 @@ +package handler + +import ( + "errors" + "net/http" + "strconv" + "time" + + "github.com/saveinmed/backend-go/internal/domain" +) + +// CreateProduct godoc +// @Summary Cadastro de produto com rastreabilidade de lote +// @Tags Produtos +// @Accept json +// @Produce json +// @Param product body registerProductRequest true "Produto" +// @Success 201 {object} domain.Product +// @Router /api/v1/products [post] +func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) { + var req registerProductRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + product := &domain.Product{ + SellerID: req.SellerID, + Name: req.Name, + Description: req.Description, + Batch: req.Batch, + ExpiresAt: req.ExpiresAt, + PriceCents: req.PriceCents, + Stock: req.Stock, + } + + if err := h.svc.RegisterProduct(r.Context(), product); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusCreated, product) +} + +// ListProducts godoc +// @Summary Lista catálogo com lote e validade +// @Tags Produtos +// @Produce json +// @Success 200 {array} domain.Product +// @Router /api/v1/products [get] +func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) { + products, err := h.svc.ListProducts(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, products) +} + +// GetProduct godoc +// @Summary Obter produto +// @Tags Produtos +// @Produce json +// @Param id path string true "Product ID" +// @Success 200 {object} domain.Product +// @Failure 404 {object} map[string]string +// @Router /api/v1/products/{id} [get] +func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) { + id, err := parseUUIDFromPath(r.URL.Path) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + product, err := h.svc.GetProduct(r.Context(), id) + if err != nil { + writeError(w, http.StatusNotFound, err) + return + } + + writeJSON(w, http.StatusOK, product) +} + +// UpdateProduct godoc +// @Summary Atualizar produto +// @Tags Produtos +// @Accept json +// @Produce json +// @Param id path string true "Product ID" +// @Param payload body updateProductRequest true "Campos para atualização" +// @Success 200 {object} domain.Product +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/v1/products/{id} [patch] +func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) { + id, err := parseUUIDFromPath(r.URL.Path) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + var req updateProductRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + product, err := h.svc.GetProduct(r.Context(), id) + if err != nil { + writeError(w, http.StatusNotFound, err) + return + } + + if req.SellerID != nil { + product.SellerID = *req.SellerID + } + if req.Name != nil { + product.Name = *req.Name + } + if req.Description != nil { + product.Description = *req.Description + } + if req.Batch != nil { + product.Batch = *req.Batch + } + if req.ExpiresAt != nil { + product.ExpiresAt = *req.ExpiresAt + } + if req.PriceCents != nil { + product.PriceCents = *req.PriceCents + } + if req.Stock != nil { + product.Stock = *req.Stock + } + + if err := h.svc.UpdateProduct(r.Context(), product); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusOK, product) +} + +// DeleteProduct godoc +// @Summary Remover produto +// @Tags Produtos +// @Param id path string true "Product ID" +// @Success 204 "" +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/v1/products/{id} [delete] +func (h *Handler) DeleteProduct(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.DeleteProduct(r.Context(), id); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// ListInventory godoc +// @Summary Listar estoque +// @Tags Estoque +// @Security BearerAuth +// @Produce json +// @Param expires_in_days query int false "Dias para expiração" +// @Success 200 {array} domain.InventoryItem +// @Router /api/v1/inventory [get] +// ListInventory exposes stock with expiring batch filters. +func (h *Handler) ListInventory(w http.ResponseWriter, r *http.Request) { + var filter domain.InventoryFilter + if days := r.URL.Query().Get("expires_in_days"); days != "" { + n, err := strconv.Atoi(days) + if err != nil || n < 0 { + writeError(w, http.StatusBadRequest, errors.New("invalid expires_in_days")) + return + } + expires := time.Now().Add(time.Duration(n) * 24 * time.Hour) + filter.ExpiringBefore = &expires + } + + inventory, err := h.svc.ListInventory(r.Context(), filter) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusOK, inventory) +} + +// AdjustInventory godoc +// @Summary Ajustar estoque +// @Tags Estoque +// @Security BearerAuth +// @Accept json +// @Produce json +// @Param payload body inventoryAdjustRequest true "Ajuste de estoque" +// @Success 200 {object} domain.InventoryItem +// @Failure 400 {object} map[string]string +// @Router /api/v1/inventory/adjust [post] +// AdjustInventory handles manual stock corrections. +func (h *Handler) AdjustInventory(w http.ResponseWriter, r *http.Request) { + var req inventoryAdjustRequest + if err := decodeJSON(r.Context(), r, &req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + if req.Delta == 0 { + writeError(w, http.StatusBadRequest, errors.New("delta must be non-zero")) + return + } + + item, err := h.svc.AdjustInventory(r.Context(), req.ProductID, req.Delta, req.Reason) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + writeJSON(w, http.StatusOK, item) +}