saveinmed/backend/internal/http/handler/dto.go
joaoaodt eb9dc8be1d fix: correcoes de acesso e marketplace
- Corrige algoritmo de validacao CNPJ (pesos completos 12/13 digitos)
- Auto-login apos cadastro de usuario redirecionando para /seller
- Registro: role padrao Seller quando campo vazio, mapeamento company_name/cnpj
- Adiciona role Seller ao middleware productManagers (fix 403 em criacao de produto)
- Inventario: usa campos corretos da API (nome, ean_code, sale_price_cents, stock_quantity)
- Marketplace: raio padrao nacional (5000km), empresas sem coordenadas sempre visiveis
- dto.go: adiciona CompanyName e CNPJ ao registerAuthRequest
2026-02-27 15:46:35 -03:00

438 lines
16 KiB
Go

package handler
import (
"context"
"encoding/json"
"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"`
CompanyName string `json:"company_name,omitempty"` // Frontend sends this
CNPJ string `json:"cnpj,omitempty"` // Frontend sends this
}
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"`
CompanyName interface{} `json:"company_name,omitempty"`
Superadmin interface{} `json:"superadmin,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"`
// Portuguese Frontend Compatibility (Partial Updates)
RazaoSocial *string `json:"razao-social,omitempty"`
NomeFantasia *string `json:"nome-fantasia,omitempty"`
DataAbertura *string `json:"data-abertura,omitempty"`
Telefone *string `json:"telefone,omitempty"`
CodigoAtividade *string `json:"codigo_atividade,omitempty"`
DescricaoAtividade *string `json:"descricao_atividade,omitempty"`
Situacao *string `json:"situacao,omitempty"`
NaturezaJuridica *string `json:"natureza-juridica,omitempty"`
Porte *string `json:"porte,omitempty"`
AtividadePrincipal *string `json:"atividade-principal,omitempty"`
AtividadePrincipalCodigo *string `json:"atividade-principal-codigo,omitempty"`
AtividadePrincipalDesc *string `json:"atividade-principal-desc,omitempty"`
Email *string `json:"email,omitempty"`
CapitalSocial *float64 `json:"capital-social,omitempty"`
AddressID *string `json:"enderecoID,omitempty"`
TipoFrete *string `json:"tipoFrete,omitempty"`
RaioEntregaKm *float64 `json:"raioEntregaKm,omitempty"`
TaxaEntrega *float64 `json:"taxaEntrega,omitempty"`
ValorFreteKm *float64 `json:"valorFreteKm,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"`
Batch string `json:"batch,omitempty"` // Compatibility
ExpiresAt string `json:"expires_at,omitempty"` // Compatibility
Stock int64 `json:"stock,omitempty"` // Compatibility
PriceCents int64 `json:"price_cents"`
SalePriceCents int64 `json:"sale_price_cents,omitempty"` // New field for frontend compatibility
// 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"`
SalePriceCents *int64 `json:"sale_price_cents,omitempty"` // Compatibility
// 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"`
}
// orderPaymentMethod handles both frontend formats:
// - Plain string: "pix" | "credit_card" | "debit_card"
// - Object: { "type": "pix", "installments": 1 }
type orderPaymentMethod struct {
Type string `json:"type"`
Installments int `json:"installments"`
}
func (m *orderPaymentMethod) UnmarshalJSON(data []byte) error {
// Try plain string first
var s string
if err := json.Unmarshal(data, &s); err == nil {
m.Type = s
m.Installments = 1
return nil
}
// Fall back to object format
type alias orderPaymentMethod
var obj alias
if err := json.Unmarshal(data, &obj); err != nil {
return err
}
*m = orderPaymentMethod(obj)
return nil
}
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 {
EntityID *uuid.UUID `json:"entity_id,omitempty"` // Allow admin to specify owner
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)
_ = 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()
// Using standard encoding/json for maximum compatibility and permissive decoding
if err := json.NewDecoder(r.Body).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
}