saveinmed/docs/API_DIVERGENCES.md

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 |