273 lines
9.2 KiB
Markdown
273 lines
9.2 KiB
Markdown
# 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
|
|
```typescript
|
|
// 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
|
|
```go
|
|
// 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):
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```go
|
|
// 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
|
|
```typescript
|
|
// 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
|
|
```go
|
|
// 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:
|
|
```typescript
|
|
payment_method: { type: 'pix', installments: 1 }
|
|
```
|
|
|
|
---
|
|
|
|
## Divergência 4 🟡 — Auth: Resposta 401 Não Limpa a Sessão
|
|
|
|
### Comportamento atual do frontend
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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:
|
|
```go
|
|
// 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
|
|
```typescript
|
|
// 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 |
|