package handler import ( "errors" "net/http" "strings" jsoniter "github.com/json-iterator/go" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" "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}) } // 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/v1/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) }