package handler import ( "context" "errors" "net/http" "strconv" "strings" "time" "github.com/gofrs/uuid/v5" "github.com/saveinmed/backend-go/internal/domain" "github.com/saveinmed/backend-go/internal/http/middleware" ) // --- Request DTOs --- type createUserRequest struct { CompanyID uuid.UUID `json:"company_id"` Role string `json:"role"` Name string `json:"name"` Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` Superadmin bool `json:"superadmin"` NomeSocial string `json:"nome-social"` CPF string `json:"cpf"` } type registerAuthRequest struct { CompanyID *uuid.UUID `json:"company_id,omitempty"` Company *registerCompanyTarget `json:"company,omitempty"` Role string `json:"role"` Name string `json:"name"` Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } type registerCompanyTarget struct { ID uuid.UUID `json:"id,omitempty"` Category string `json:"category"` CNPJ string `json:"cnpj"` CorporateName string `json:"corporate_name"` LicenseNumber string `json:"license_number"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` City string `json:"city"` State string `json:"state"` } type loginRequest struct { Username string `json:"username,omitempty"` Email string `json:"email,omitempty"` Password string `json:"password"` } type forgotPasswordRequest struct { Email string `json:"email"` } type resetPasswordRequest struct { Token string `json:"token"` Password string `json:"password"` } type verifyEmailRequest struct { Token string `json:"token"` } type authResponse struct { Token string `json:"access_token"` ExpiresAt time.Time `json:"expires_at"` } type messageResponse struct { Message string `json:"message"` } type resetTokenResponse struct { Message string `json:"message"` ResetToken string `json:"reset_token,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` } 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"` Username *string `json:"username,omitempty"` Email *string `json:"email,omitempty"` Password *string `json:"password,omitempty"` EmpresasDados []string `json:"empresasDados"` // Frontend sends array of strings Enderecos []string `json:"enderecos"` // Frontend sends array of strings // Ignored fields sent by frontend to prevent "unknown field" errors ID interface{} `json:"id,omitempty"` EmailVerified interface{} `json:"email_verified,omitempty"` CreatedAt interface{} `json:"created_at,omitempty"` UpdatedAt interface{} `json:"updated_at,omitempty"` Nome interface{} `json:"nome,omitempty"` Ativo interface{} `json:"ativo,omitempty"` CPF interface{} `json:"cpf,omitempty"` NomeSocial interface{} `json:"nome-social,omitempty"` RegistroCompleto interface{} `json:"registro-completo,omitempty"` Nivel interface{} `json:"nivel,omitempty"` } type requester struct { ID uuid.UUID Role string CompanyID *uuid.UUID } type registerCompanyRequest struct { Category string `json:"category"` CNPJ string `json:"cnpj"` CorporateName string `json:"corporate_name"` LicenseNumber string `json:"license_number"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` City string `json:"city"` State string `json:"state"` // Portuguese Frontend Compatibility RazaoSocial string `json:"razao-social"` NomeFantasia string `json:"nome-fantasia"` DataAbertura string `json:"data-abertura"` // Fixed: frontend sends hyphen Telefone string `json:"telefone"` CodigoAtividade string `json:"codigo_atividade"` DescricaoAtividade string `json:"descricao_atividade"` Situacao string `json:"situacao"` // Ignored for now NaturezaJuridica string `json:"natureza-juridica"` // Ignored for now Porte string `json:"porte"` // Ignored for now AtividadePrincipal string `json:"atividade-principal"` // Frontend might send this AtividadePrincipalCodigo string `json:"atividade-principal-codigo"` // Frontend sends this AtividadePrincipalDesc string `json:"atividade-principal-desc"` // Frontend sends this Email string `json:"email"` // Frontend sends this CapitalSocial float64 `json:"capital-social"` // Frontend sends this (number) AddressID string `json:"enderecoID"` // Frontend sends this TipoFrete string `json:"tipoFrete"` // Frontend sends this RaioEntregaKm float64 `json:"raioEntregaKm"` // Frontend sends this TaxaEntrega float64 `json:"taxaEntrega"` // Frontend sends this ValorFreteKm float64 `json:"valorFreteKm"` // Frontend sends this } type updateCompanyRequest struct { Category *string `json:"category,omitempty"` CNPJ *string `json:"cnpj,omitempty"` CorporateName *string `json:"corporate_name,omitempty"` LicenseNumber *string `json:"license_number,omitempty"` IsVerified *bool `json:"is_verified,omitempty"` Latitude *float64 `json:"latitude,omitempty"` Longitude *float64 `json:"longitude,omitempty"` City *string `json:"city,omitempty"` State *string `json:"state,omitempty"` } type registerProductRequest struct { SellerID uuid.UUID `json:"seller_id"` EANCode string `json:"ean_code"` Name string `json:"name"` Description string `json:"description"` Manufacturer string `json:"manufacturer"` Category string `json:"category"` Subcategory string `json:"subcategory"` PriceCents int64 `json:"price_cents"` // New Fields InternalCode string `json:"internal_code"` FactoryPriceCents int64 `json:"factory_price_cents"` PMCCents int64 `json:"pmc_cents"` CommercialDiscountCents int64 `json:"commercial_discount_cents"` TaxSubstitutionCents int64 `json:"tax_substitution_cents"` InvoicePriceCents int64 `json:"invoice_price_cents"` } type updateProductRequest struct { SellerID *uuid.UUID `json:"seller_id,omitempty"` EANCode *string `json:"ean_code,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Manufacturer *string `json:"manufacturer,omitempty"` Category *string `json:"category,omitempty"` Subcategory *string `json:"subcategory,omitempty"` PriceCents *int64 `json:"price_cents,omitempty"` // New Fields InternalCode *string `json:"internal_code,omitempty"` FactoryPriceCents *int64 `json:"factory_price_cents,omitempty"` PMCCents *int64 `json:"pmc_cents,omitempty"` CommercialDiscountCents *int64 `json:"commercial_discount_cents,omitempty"` TaxSubstitutionCents *int64 `json:"tax_substitution_cents,omitempty"` InvoicePriceCents *int64 `json:"invoice_price_cents,omitempty"` Stock *int64 `json:"qtdade_estoque,omitempty"` // Frontend compatibility PrecoVenda *float64 `json:"preco_venda,omitempty"` // Frontend compatibility (float) } 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"` PaymentMethod orderPaymentMethod `json:"payment_method"` } type orderPaymentMethod struct { Type string `json:"type"` Installments int `json:"installments"` } 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"` } type shippingSettingsRequest struct { Active bool `json:"active"` MaxRadiusKm float64 `json:"max_radius_km"` PricePerKmCents int64 `json:"price_per_km_cents"` MinFeeCents int64 `json:"min_fee_cents"` FreeShippingThresholdCents *int64 `json:"free_shipping_threshold_cents,omitempty"` PickupActive bool `json:"pickup_active"` PickupAddress string `json:"pickup_address,omitempty"` PickupHours string `json:"pickup_hours,omitempty"` Latitude float64 `json:"latitude"` // Store location for radius calc Longitude float64 `json:"longitude"` } type shippingCalculateRequest struct { VendorID uuid.UUID `json:"vendor_id"` CartTotalCents int64 `json:"cart_total_cents"` BuyerLatitude *float64 `json:"buyer_latitude,omitempty"` BuyerLongitude *float64 `json:"buyer_longitude,omitempty"` AddressID *uuid.UUID `json:"address_id,omitempty"` PostalCode string `json:"postal_code,omitempty"` } type createAddressRequest struct { Title string `json:"titulo"` ZipCode string `json:"cep"` Street string `json:"logradouro"` Number string `json:"numero"` Complement string `json:"complemento"` District string `json:"bairro"` City string `json:"cidade"` State string `json:"estado"` // JSON from frontend sends "estado" Country string `json:"pais"` // JSON includes "pais" } // --- Utility Functions --- func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = jsonAPI.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 := jsonAPI.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 } } 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{ID: claims.UserID, 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 } func parseBearerToken(r *http.Request) (string, error) { authHeader := r.Header.Get("Authorization") if authHeader == "" { return "", errors.New("missing Authorization header") } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") { return "", errors.New("invalid Authorization header") } token := strings.TrimSpace(parts[1]) if token == "" { return "", errors.New("token is required") } return token, nil }