Backend: - Adição das migrações SQL 0012 e 0013 para estrutura de produtos e itens de estoque. - Implementação do método [CreateInventoryItem](cci:1://file:///c:/Projetos/saveinmed/backend-old/internal/http/handler/handler_test.go:168:0-170:1) no repositório Postgres e mocks de teste. - Atualização do [product_handler.go](cci:7://file:///c:/Projetos/saveinmed/backend-old/internal/http/handler/product_handler.go:0:0-0:0) para suportar `original_price_cents` e corrigir filtragem de estoque. - Mapeamento da rota GET `/api/v1/produtos-venda` no [server.go](cci:7://file:///c:/Projetos/saveinmed/backend-old/internal/server/server.go:0:0-0:0). - Ajuste no endpoint `/auth/me` para retornar `empresasDados` (ID da empresa) necessário ao frontend. - Refatoração da query [ListInventory](cci:1://file:///c:/Projetos/saveinmed/backend-old/internal/repository/postgres/postgres.go:771:0-805:1) para buscar da tabela correta e incluir nome do produto. Frontend: - Correção no mapeamento de dados (snake_case para camelCase) na página de Gestão de Produtos. - Ajustes de integração no Wizard de Cadastro de Produtos (`CadastroProdutoWizard.tsx`). - Atualização da tipagem para exibir corretamente preços e estoque a partir da API.
376 lines
13 KiB
Go
376 lines
13 KiB
Go
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"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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 domain.PaymentMethod `json:"payment_method"`
|
|
}
|
|
|
|
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
|
|
}
|