saveinmed/docs/API_DIVERGENCES.md

9.2 KiB

Divergências Frontend ↔ Backend — SaveInMed

Análise realizada em 25/02/2026 Branch analisada: chore/monorepo-cleanup-and-structure


Resumo Executivo

# Fluxo Severidade Impacto
1 Cálculo de Frete — request body 🔴 CRÍTICO Frontend e backend são completamente incompatíveis; nenhuma requisição de cálculo de frete funciona
2 Cálculo de Frete — response body 🔴 CRÍTICO Frontend lê campos que não existem na resposta do backend
3 Criação de Pedido — payment_method 🔴 CRÍTICO Frontend envia string, backend espera {type, installments}; campo ignorado silenciosamente
4 Auth 401 — limpeza de sessão 🟡 MÉDIO 401 loga aviso mas não invalida token local; usuário fica preso com sessão expirada
5 Criação de Pedido — buyer_id 🟢 BAIXO Frontend envia buyer_id no body, mas backend usa JWT claims; campo extra inofensivo
6 Paginação de pedidos 🟢 BAIXO Frontend chama listOrders() sem parâmetros de paginação; backend usa padrão page=1/size=20

Divergência 1 🔴 — Cálculo de Frete: Request Body

O que o Frontend envia

// frontend/src/services/shippingService.ts
POST /api/v1/shipping/calculate
{
  buyer_id: string,          // ← NÃO existe no backend
  order_total_cents: number, // ← backend chama de: cart_total_cents
  items: [                   // ← NÃO existe no backend
    {
      seller_id: string,
      product_id: string,
      quantity: number,
      price_cents: number
    }
  ]
}

O que o Backend espera

// backend/internal/http/handler/dto.go
type shippingCalculateRequest struct {
  VendorID       uuid.UUID  `json:"vendor_id"`               // ← NÃO enviado pelo frontend
  CartTotalCents int64      `json:"cart_total_cents"`         // ← frontend usa: order_total_cents
  BuyerLatitude  *float64   `json:"buyer_latitude,omitempty"` // ← NÃO enviado pelo frontend
  BuyerLongitude *float64   `json:"buyer_longitude,omitempty"`// ← NÃO enviado pelo frontend
  AddressID      *uuid.UUID `json:"address_id,omitempty"`
  PostalCode     string     `json:"postal_code,omitempty"`
}

Efeito em Produção

O backend rejeita todas as requisições com 400 Bad Request: "vendor_id is required" pois o campo vendor_id nunca é enviado. O cálculo de frete jamais funciona.

Correção sugerida (duas opções)

Opção A — Corrigir o frontend (menor esforço):

// Substituir CalculateShippingRequest no frontend por:
interface CalculateShippingRequest {
  vendor_id: string
  cart_total_cents: number
  buyer_latitude: number
  buyer_longitude: number
}

Opção B — Adaptar o backend para aceitar o formato atual do frontend (mais trabalho).


Divergência 2 🔴 — Cálculo de Frete: Response Body

O que o Frontend espera receber

// frontend/src/services/shippingService.ts
interface CalculateShippingResponse {
  options: {
    seller_id: string          // ← campo ausente na resposta do backend
    delivery_fee_cents: number // ← backend retorna: value_cents
    distance_km: number        // ← campo ausente na resposta do backend
    estimated_days: number     // ← backend retorna: estimated_minutes (em minutos!)
    pickup_available: boolean  // ← campo ausente; backend usa type="pickup" para indicar
    pickup_address?: string
    pickup_hours?: string
  }[]
}

O que o Backend realmente retorna

// backend/internal/http/handler/shipping_handler.go + domain.ShippingOption
[]domain.ShippingOption{
  {
    Type:             "delivery" | "pickup",
    Description:      "Entrega padrão" | "Frete Grátis",
    ValueCents:       fee,          // ← frontend espera: delivery_fee_cents
    EstimatedMinutes: 120,          // ← hardcoded; frontend espera: estimated_days
  }
}

Efeito em Produção

O frontend sempre exibe undefined para o valor do frete, distância e prazo estimado.

Correção sugerida

Padronizar o response do backend para incluir todos os campos esperados pelo frontend, ou criar um adapter no frontend que converta ShippingOption para CalculateShippingResponse.


Divergência 3 🔴 — Criação de Pedido: payment_method

O que o Frontend envia

// frontend/src/services/ordersService.ts
interface CreateOrderRequest {
  // ...
  payment_method: 'pix' | 'credit_card' | 'debit_card'  // ← string direta
}

// Exemplo real:
{ payment_method: "pix" }

O que o Backend espera

// backend/internal/http/handler/dto.go
type createOrderRequest struct {
  // ...
  PaymentMethod orderPaymentMethod `json:"payment_method"`  // ← objeto!
}

type orderPaymentMethod struct {
  Type         string `json:"type"`
  Installments int    `json:"installments"`
}

// Exemplo esperado:
{ "payment_method": { "type": "pix", "installments": 1 } }

O que acontece em produção

O JSON decoder do Go não falha explicitamente (campo incompatível → zero value), então PaymentMethod.Type fica vazio (""). O pedido é criado sem método de pagamento definido, causando falhas silenciosas no processamento de pagamento.

Correção sugerida

Unificar o contrato. Recomendado: corrigir o frontend para enviar objeto:

payment_method: { type: 'pix', installments: 1 }

Divergência 4 🟡 — Auth: Resposta 401 Não Limpa a Sessão

Comportamento atual do frontend

// frontend/src/services/apiClient.ts
instance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      logger.warn('Sessão expirada, por favor, faça login novamente.')
      // ← NÃO limpa o token, NÃO redireciona, NÃO chama logout
    }
    return Promise.reject(error)
  }
)

Comportamento esperado

Quando o backend retorna 401:

  1. Limpar o token do localStorage
  2. Zerar o token no apiClient
  3. Redirecionar o usuário para /login

Efeito em Produção

Usuário com token expirado continua fazendo requisições que falham com 401 indefinidamente até recarregar a página manualmente.

Correção sugerida

// No interceptor de response do apiClient:
if (error.response?.status === 401) {
  apiClient.setToken(null)
  localStorage.removeItem('mp-auth-user')
  window.location.href = '/login'
}

Divergência 5 🟢 — Order: buyer_id enviado mas ignorado

Comportamento

O frontend envia buyer_id no body do pedido, mas o backend extrai o comprador do JWT:

// backend/internal/http/handler/order_handler.go
claims, ok := middleware.GetClaims(r.Context())
order.BuyerID = *claims.CompanyID  // ← usa JWT, ignora o buyer_id do body

Risco

Baixo. O backend usa sempre o JWT (mais seguro). O campo extra no body é inofensivo. Porém, cria confusão e falsa sensação de que o buyer_id no body tem efeito.


Divergência 6 🟢 — Paginação de Pedidos: Sem Parâmetros

Comportamento

// frontend/src/services/ordersService.ts
listOrders: () => apiClient.get('/v1/orders')
// ← sem page, page_size, role

O backend suporta ?page=N&page_size=M&role=buyer|seller mas o frontend nunca envia. Resultado: sempre retorna a primeira página com 20 itens.


Mapeamento de Campos Corretos

Shipping Settings ( alinhados)

Campo Frontend Campo Backend OK?
vendor_id vendor_id
active active
max_radius_km max_radius_km
price_per_km_cents price_per_km_cents
min_fee_cents min_fee_cents
pickup_active pickup_active
pickup_address pickup_address
pickup_hours pickup_hours

Auth ( alinhados)

Campo Frontend Campo Backend OK?
username username
password password
access_token access_token
expires_at expires_at (time.Time → RFC3339)

Shipping Calculate Request (🔴 divergentes)

Campo Frontend Campo Backend OK?
buyer_id — (não existe) 🔴
order_total_cents cart_total_cents 🔴 nome diferente
items[] — (não existe) 🔴
vendor_id (obrigatório) 🔴 não enviado
buyer_latitude (obrigatório) 🔴 não enviado
buyer_longitude (obrigatório) 🔴 não enviado

Shipping Calculate Response (🔴 divergentes)

Campo Resposta Backend Campo Esperado Frontend OK?
type
description
value_cents delivery_fee_cents 🔴 nome diferente
estimated_minutes estimated_days 🔴 nome e unidade diferentes
seller_id 🔴 ausente
distance_km 🔴 ausente
pickup_available 🔴 ausente

Order Create (🔴 divergente em payment_method)

Campo Frontend Campo Backend OK?
seller_id seller_id
items[] items[] (domain.OrderItem)
shipping.recipient_name shipping.recipient_name
shipping.street shipping.street
payment_method: "pix" payment_method: {type, installments} 🔴 tipo errado
buyer_id ignorado (usa JWT) 🟡 inofensivo