saveinmed/backend/internal/http/handler/handler.go
2025-12-18 12:29:51 -03:00

718 lines
18 KiB
Go

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 handles sign-up creating a company when requested.
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,
}
}
user := &domain.User{
CompanyID: req.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 validates credentials and emits a JWT token.
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)
}
// 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 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)
}
// 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)
}
// 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,
}
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)
}
// GetOrder godoc
// @Summary Consulta pedido
// @Tags Pedidos
// @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
// @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)
}
// CreatePaymentPreference godoc
// @Summary Cria preferência de pagamento Mercado Pago com split nativo
// @Tags Pagamentos
// @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)
}
// CreateUser handles the creation of platform users.
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 supports pagination and optional company filter.
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 returns a single user by ID.
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 updates profile fields or password.
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 removes a user by ID.
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 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 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 createOrderRequest struct {
BuyerID uuid.UUID `json:"buyer_id"`
SellerID uuid.UUID `json:"seller_id"`
Items []domain.OrderItem `json:"items"`
}
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
}
}