package handler import ( "context" "errors" "net/http" "strconv" "strings" "time" jsoniter "github.com/json-iterator/go" "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" ) var json = jsoniter.ConfigCompatibleWithStandardLibrary type Handler struct { svc *usecase.Service } func New(svc *usecase.Service) *Handler { return &Handler{svc: svc} } // Register godoc // @Summary Cadastro de usuário // @Description Cria um usuário e opcionalmente uma empresa, retornando token JWT. // @Tags Autenticação // @Accept json // @Produce json // @Param payload body registerAuthRequest true "Dados do usuário e empresa" // @Success 201 {object} authResponse // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/auth/register [post] func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { var req registerAuthRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } var company *domain.Company if req.Company != nil { company = &domain.Company{ ID: req.Company.ID, Role: req.Company.Role, CNPJ: req.Company.CNPJ, CorporateName: req.Company.CorporateName, LicenseNumber: req.Company.LicenseNumber, } } var companyID uuid.UUID if req.CompanyID != nil { companyID = *req.CompanyID } user := &domain.User{ CompanyID: companyID, Role: req.Role, Name: req.Name, Email: req.Email, } if user.CompanyID == uuid.Nil && company == nil { writeError(w, http.StatusBadRequest, errors.New("company_id or company payload is required")) return } if err := h.svc.RegisterAccount(r.Context(), company, user, req.Password); err != nil { writeError(w, http.StatusInternalServerError, err) return } token, exp, err := h.svc.Authenticate(r.Context(), user.Email, req.Password) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, authResponse{Token: token, ExpiresAt: exp}) } // Login godoc // @Summary Login // @Description Autentica usuário e retorna token JWT. // @Tags Autenticação // @Accept json // @Produce json // @Param payload body loginRequest true "Credenciais" // @Success 200 {object} authResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/v1/auth/login [post] func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var req loginRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } token, exp, err := h.svc.Authenticate(r.Context(), req.Email, req.Password) if err != nil { writeError(w, http.StatusUnauthorized, err) return } writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp}) } // CreateCompany godoc // @Summary Registro de empresas // @Description Cadastra farmácia, distribuidora ou administrador com CNPJ e licença sanitária. // @Tags Empresas // @Accept json // @Produce json // @Param company body registerCompanyRequest true "Dados da empresa" // @Success 201 {object} domain.Company // @Router /api/companies [post] func (h *Handler) CreateCompany(w http.ResponseWriter, r *http.Request) { var req registerCompanyRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } company := &domain.Company{ Role: req.Role, CNPJ: req.CNPJ, CorporateName: req.CorporateName, LicenseNumber: req.LicenseNumber, } if err := h.svc.RegisterCompany(r.Context(), company); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, company) } // ListCompanies godoc // @Summary Lista empresas // @Tags Empresas // @Produce json // @Success 200 {array} domain.Company // @Router /api/companies [get] func (h *Handler) ListCompanies(w http.ResponseWriter, r *http.Request) { companies, err := h.svc.ListCompanies(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, companies) } // GetCompany godoc // @Summary Obter empresa // @Tags Empresas // @Produce json // @Param id path string true "Company ID" // @Success 200 {object} domain.Company // @Failure 404 {object} map[string]string // @Router /api/companies/{id} [get] func (h *Handler) GetCompany(w http.ResponseWriter, r *http.Request) { id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } company, err := h.svc.GetCompany(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err) return } writeJSON(w, http.StatusOK, company) } // UpdateCompany godoc // @Summary Atualizar empresa // @Tags Empresas // @Accept json // @Produce json // @Param id path string true "Company ID" // @Param payload body updateCompanyRequest true "Campos para atualização" // @Success 200 {object} domain.Company // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/companies/{id} [patch] func (h *Handler) UpdateCompany(w http.ResponseWriter, r *http.Request) { id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } var req updateCompanyRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } company, err := h.svc.GetCompany(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err) return } if req.Role != nil { company.Role = *req.Role } if req.CNPJ != nil { company.CNPJ = *req.CNPJ } if req.CorporateName != nil { company.CorporateName = *req.CorporateName } if req.LicenseNumber != nil { company.LicenseNumber = *req.LicenseNumber } if req.IsVerified != nil { company.IsVerified = *req.IsVerified } if err := h.svc.UpdateCompany(r.Context(), company); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, company) } // DeleteCompany godoc // @Summary Remover empresa // @Tags Empresas // @Param id path string true "Company ID" // @Success 204 "" // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/companies/{id} [delete] func (h *Handler) DeleteCompany(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.DeleteCompany(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err) return } w.WriteHeader(http.StatusNoContent) } // VerifyCompany godoc // @Summary Verificar empresa // @Tags Empresas // @Security BearerAuth // @Param id path string true "Company ID" // @Success 200 {object} domain.Company // @Router /api/v1/companies/{id}/verify [patch] // VerifyCompany toggles the verification flag for a company (admin only). func (h *Handler) VerifyCompany(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/verify") { http.NotFound(w, r) return } id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } company, err := h.svc.VerifyCompany(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, company) } // GetMyCompany godoc // @Summary Obter minha empresa // @Tags Empresas // @Security BearerAuth // @Produce json // @Success 200 {object} domain.Company // @Router /api/v1/companies/me [get] // GetMyCompany returns the company linked to the authenticated user. func (h *Handler) GetMyCompany(w http.ResponseWriter, r *http.Request) { claims, ok := middleware.GetClaims(r.Context()) if !ok || claims.CompanyID == nil { writeError(w, http.StatusBadRequest, errors.New("missing company context")) return } company, err := h.svc.GetCompany(r.Context(), *claims.CompanyID) if err != nil { writeError(w, http.StatusNotFound, err) return } writeJSON(w, http.StatusOK, company) } // GetCompanyRating godoc // @Summary Obter avaliação da empresa // @Tags Empresas // @Produce json // @Param id path string true "Company ID" // @Success 200 {object} domain.CompanyRating // @Router /api/v1/companies/{id}/rating [get] // GetCompanyRating exposes the average score for a company. func (h *Handler) GetCompanyRating(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/rating") { http.NotFound(w, r) return } id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } rating, err := h.svc.GetCompanyRating(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, rating) } // 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/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/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/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/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/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 // @Accept json // @Produce json // @Param order body createOrderRequest true "Pedido" // @Success 201 {object} domain.Order // @Router /api/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/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/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/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/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 // @Security BearerAuth // @Produce json // @Param id path string true "Order ID" // @Success 201 {object} domain.PaymentPreference // @Router /api/orders/{id}/payment [post] func (h *Handler) CreatePaymentPreference(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/payment") { http.NotFound(w, r) return } id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } pref, err := h.svc.CreatePaymentPreference(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, pref) } // CreateShipment godoc // @Summary Gera guia de postagem/transporte // @Tags Logistica // @Security BearerAuth // @Accept json // @Produce json // @Param shipment body createShipmentRequest true "Dados de envio" // @Success 201 {object} domain.Shipment // @Router /api/v1/shipments [post] func (h *Handler) CreateShipment(w http.ResponseWriter, r *http.Request) { var req createShipmentRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } shipment := &domain.Shipment{ OrderID: req.OrderID, Carrier: req.Carrier, TrackingCode: req.TrackingCode, ExternalTracking: req.ExternalTracking, } if err := h.svc.CreateShipment(r.Context(), shipment); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, shipment) } // GetShipmentByOrderID godoc // @Summary Rastreia entrega // @Tags Logistica // @Security BearerAuth // @Produce json // @Param order_id path string true "Order ID" // @Success 200 {object} domain.Shipment // @Router /api/v1/shipments/{order_id} [get] func (h *Handler) GetShipmentByOrderID(w http.ResponseWriter, r *http.Request) { orderID, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } shipment, err := h.svc.GetShipmentByOrderID(r.Context(), orderID) if err != nil { writeError(w, http.StatusNotFound, err) return } writeJSON(w, http.StatusOK, shipment) } // HandlePaymentWebhook godoc // @Summary Recebe notificações do Mercado Pago // @Tags Pagamentos // @Accept json // @Produce json // @Param notification body domain.PaymentWebhookEvent true "Evento do gateway" // @Success 200 {object} domain.PaymentSplitResult // @Router /api/v1/payments/webhook [post] func (h *Handler) HandlePaymentWebhook(w http.ResponseWriter, r *http.Request) { var event domain.PaymentWebhookEvent if err := decodeJSON(r.Context(), r, &event); err != nil { writeError(w, http.StatusBadRequest, err) return } summary, err := h.svc.HandlePaymentWebhook(r.Context(), event) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, summary) } // GetSellerDashboard aggregates KPIs for the authenticated seller or the requested company. // @Summary Dashboard do vendedor // @Tags Dashboard // @Security BearerAuth // @Produce json // @Param seller_id query string false "Seller ID" // @Success 200 {object} domain.SellerDashboard // @Router /api/v1/dashboard/seller [get] func (h *Handler) GetSellerDashboard(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } sellerID := requester.CompanyID if sid := r.URL.Query().Get("seller_id"); sid != "" { id, err := uuid.FromString(sid) if err != nil { writeError(w, http.StatusBadRequest, errors.New("invalid seller_id")) return } sellerID = &id } if sellerID == nil { writeError(w, http.StatusBadRequest, errors.New("seller context is required")) return } if !strings.EqualFold(requester.Role, "Admin") && requester.CompanyID != nil && *sellerID != *requester.CompanyID { writeError(w, http.StatusForbidden, errors.New("not allowed to view other sellers")) return } dashboard, err := h.svc.GetSellerDashboard(r.Context(), *sellerID) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, dashboard) } // GetAdminDashboard exposes platform-wide aggregated metrics. // @Summary Dashboard do administrador // @Tags Dashboard // @Security BearerAuth // @Produce json // @Success 200 {object} domain.AdminDashboard // @Router /api/v1/dashboard/admin [get] func (h *Handler) GetAdminDashboard(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } if !strings.EqualFold(requester.Role, "Admin") { writeError(w, http.StatusForbidden, errors.New("admin role required")) return } dashboard, err := h.svc.GetAdminDashboard(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, dashboard) } // CreateUser godoc // @Summary Criar usuário // @Tags Usuários // @Security BearerAuth // @Accept json // @Produce json // @Param payload body createUserRequest true "Novo usuário" // @Success 201 {object} domain.User // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/users [post] func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } var req createUserRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } if strings.EqualFold(requester.Role, "Seller") { if requester.CompanyID == nil { writeError(w, http.StatusBadRequest, errors.New("seller must include X-Company-ID header")) return } if req.CompanyID != *requester.CompanyID { writeError(w, http.StatusForbidden, errors.New("seller can only manage their own company users")) return } } user := &domain.User{ CompanyID: req.CompanyID, Role: req.Role, Name: req.Name, Email: req.Email, } if err := h.svc.CreateUser(r.Context(), user, req.Password); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusCreated, user) } // ListUsers godoc // @Summary Listar usuários // @Tags Usuários // @Security BearerAuth // @Produce json // @Param page query int false "Página" // @Param page_size query int false "Tamanho da página" // @Param company_id query string false "Filtro por empresa" // @Success 200 {object} domain.UserPage // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/users [get] func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } page, pageSize := parsePagination(r) var companyFilter *uuid.UUID if cid := r.URL.Query().Get("company_id"); cid != "" { id, err := uuid.FromString(cid) if err != nil { writeError(w, http.StatusBadRequest, errors.New("invalid company_id")) return } companyFilter = &id } if strings.EqualFold(requester.Role, "Seller") { if requester.CompanyID == nil { writeError(w, http.StatusBadRequest, errors.New("seller must include X-Company-ID header")) return } companyFilter = requester.CompanyID } pageResult, err := h.svc.ListUsers(r.Context(), domain.UserFilter{CompanyID: companyFilter}, page, pageSize) if err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, pageResult) } // GetUser godoc // @Summary Obter usuário // @Tags Usuários // @Security BearerAuth // @Produce json // @Param id path string true "User ID" // @Success 200 {object} domain.User // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/users/{id} [get] func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } user, err := h.svc.GetUser(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err) return } if strings.EqualFold(requester.Role, "Seller") { if requester.CompanyID == nil || user.CompanyID != *requester.CompanyID { writeError(w, http.StatusForbidden, errors.New("seller can only view users from their company")) return } } writeJSON(w, http.StatusOK, user) } // UpdateUser godoc // @Summary Atualizar usuário // @Tags Usuários // @Security BearerAuth // @Accept json // @Produce json // @Param id path string true "User ID" // @Param payload body updateUserRequest true "Campos para atualização" // @Success 200 {object} domain.User // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/users/{id} [put] func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } var req updateUserRequest if err := decodeJSON(r.Context(), r, &req); err != nil { writeError(w, http.StatusBadRequest, err) return } user, err := h.svc.GetUser(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err) return } if strings.EqualFold(requester.Role, "Seller") { if requester.CompanyID == nil || user.CompanyID != *requester.CompanyID { writeError(w, http.StatusForbidden, errors.New("seller can only update their company users")) return } } if req.CompanyID != nil { user.CompanyID = *req.CompanyID } if req.Role != nil { user.Role = *req.Role } if req.Name != nil { user.Name = *req.Name } if req.Email != nil { user.Email = *req.Email } newPassword := "" if req.Password != nil { newPassword = *req.Password } if err := h.svc.UpdateUser(r.Context(), user, newPassword); err != nil { writeError(w, http.StatusInternalServerError, err) return } writeJSON(w, http.StatusOK, user) } // DeleteUser godoc // @Summary Excluir usuário // @Tags Usuários // @Security BearerAuth // @Param id path string true "User ID" // @Success 204 {string} string "No Content" // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/users/{id} [delete] func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) { requester, err := getRequester(r) if err != nil { writeError(w, http.StatusBadRequest, err) return } id, err := parseUUIDFromPath(r.URL.Path) if err != nil { writeError(w, http.StatusBadRequest, err) return } user, err := h.svc.GetUser(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err) return } if strings.EqualFold(requester.Role, "Seller") { if requester.CompanyID == nil || user.CompanyID != *requester.CompanyID { writeError(w, http.StatusForbidden, errors.New("seller can only delete their company users")) return } } if err := h.svc.DeleteUser(r.Context(), id); err != nil { writeError(w, http.StatusInternalServerError, err) return } w.WriteHeader(http.StatusNoContent) } type createUserRequest struct { CompanyID uuid.UUID `json:"company_id"` Role string `json:"role"` Name string `json:"name"` Email string `json:"email"` Password string `json:"password"` } type registerAuthRequest struct { CompanyID *uuid.UUID `json:"company_id,omitempty"` Company *registerCompanyTarget `json:"company,omitempty"` Role string `json:"role"` Name string `json:"name"` Email string `json:"email"` Password string `json:"password"` } type registerCompanyTarget struct { ID uuid.UUID `json:"id,omitempty"` Role string `json:"role"` CNPJ string `json:"cnpj"` CorporateName string `json:"corporate_name"` LicenseNumber string `json:"license_number"` } type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } type authResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` } type inventoryAdjustRequest struct { ProductID uuid.UUID `json:"product_id"` Delta int64 `json:"delta"` Reason string `json:"reason"` } type addCartItemRequest struct { ProductID uuid.UUID `json:"product_id"` Quantity int64 `json:"quantity"` } type createReviewRequest struct { OrderID uuid.UUID `json:"order_id"` Rating int `json:"rating"` Comment string `json:"comment"` } type updateUserRequest struct { CompanyID *uuid.UUID `json:"company_id,omitempty"` Role *string `json:"role,omitempty"` Name *string `json:"name,omitempty"` Email *string `json:"email,omitempty"` Password *string `json:"password,omitempty"` } type requester struct { Role string CompanyID *uuid.UUID } func parsePagination(r *http.Request) (int, int) { page := 1 pageSize := 20 if v := r.URL.Query().Get("page"); v != "" { if p, err := strconv.Atoi(v); err == nil && p > 0 { page = p } } if v := r.URL.Query().Get("page_size"); v != "" { if ps, err := strconv.Atoi(v); err == nil && ps > 0 { pageSize = ps } } return page, pageSize } func getRequester(r *http.Request) (requester, error) { if claims, ok := middleware.GetClaims(r.Context()); ok { return requester{Role: claims.Role, CompanyID: claims.CompanyID}, nil } role := r.Header.Get("X-User-Role") if role == "" { role = "Admin" } var companyID *uuid.UUID if cid := r.Header.Get("X-Company-ID"); cid != "" { id, err := uuid.FromString(cid) if err != nil { return requester{}, errors.New("invalid X-Company-ID header") } companyID = &id } return requester{Role: role, CompanyID: companyID}, nil } type registerCompanyRequest struct { Role string `json:"role"` CNPJ string `json:"cnpj"` CorporateName string `json:"corporate_name"` LicenseNumber string `json:"license_number"` } type updateCompanyRequest struct { Role *string `json:"role,omitempty"` CNPJ *string `json:"cnpj,omitempty"` CorporateName *string `json:"corporate_name,omitempty"` LicenseNumber *string `json:"license_number,omitempty"` IsVerified *bool `json:"is_verified,omitempty"` } type registerProductRequest struct { SellerID uuid.UUID `json:"seller_id"` Name string `json:"name"` Description string `json:"description"` Batch string `json:"batch"` ExpiresAt time.Time `json:"expires_at"` PriceCents int64 `json:"price_cents"` Stock int64 `json:"stock"` } type updateProductRequest struct { SellerID *uuid.UUID `json:"seller_id,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Batch *string `json:"batch,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` PriceCents *int64 `json:"price_cents,omitempty"` Stock *int64 `json:"stock,omitempty"` } type createOrderRequest struct { BuyerID uuid.UUID `json:"buyer_id"` SellerID uuid.UUID `json:"seller_id"` Items []domain.OrderItem `json:"items"` Shipping domain.ShippingAddress `json:"shipping"` } type createShipmentRequest struct { OrderID uuid.UUID `json:"order_id"` Carrier string `json:"carrier"` TrackingCode string `json:"tracking_code"` ExternalTracking string `json:"external_tracking"` } type updateStatusRequest struct { Status string `json:"status"` } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, err error) { writeJSON(w, status, map[string]string{"error": err.Error()}) } func decodeJSON(ctx context.Context, r *http.Request, v any) error { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(v); err != nil { return err } return ctx.Err() } func parseUUIDFromPath(path string) (uuid.UUID, error) { parts := splitPath(path) for i := len(parts) - 1; i >= 0; i-- { if id, err := uuid.FromString(parts[i]); err == nil { return id, nil } } return uuid.UUID{}, errors.New("missing resource id") } func splitPath(p string) []string { var parts []string start := 0 for i := 0; i < len(p); i++ { if p[i] == '/' { if i > start { parts = append(parts, p[start:i]) } start = i + 1 } } if start < len(p) { parts = append(parts, p[start:]) } return parts } func isValidStatus(status string) bool { switch domain.OrderStatus(status) { case domain.OrderStatusPending, domain.OrderStatusPaid, domain.OrderStatusInvoiced, domain.OrderStatusDelivered: return true default: return false } }