Exercício Abrangente da Fase 2: Refinando as Funções de Negócio Principais do OrderFlow
A integração é o teste definitivo do entendimento—combinando JPA, transações, validação, tratamento de exceções e segurança em um único sistema totalmente funcional.
1. O Que Você Vai Aprender
- Implementar joins de múltiplas tabelas e operações em cascata entre Order, OrderItem e Product
- Aprimorar o processo de fazer pedido: Verificar estoque → Deduzir estoque → Criar pedido → Garantia de transação
- Tratamento de exceções unificado cobre todos os cenários de negócio
- Controlar permissões de API por papel (CUSTOMER / ADMIN)
- Usar o console H2 e Postman para realizar testes de processo de negócio de ponta a ponta
2. Uma História Real de um Desenvolvedor Full-Stack
(1) Ponto de Dor: Módulos Operando Isoladamente
Alice completou seus estudos nos cinco módulos—JPA, Service, Validation, Exception Handling e Security—mas todos existem isoladamente. Ao fazer um pedido de verdade, ela precisa vincular todo o processo: "validar entrada → verificar estoque → deduzir estoque → criar pedido → garantir integridade da transação → impor controle de acesso → tratar exceções." Ela não tem certeza de como os vários módulos trabalham juntos.
(2) Soluções para os Exercícios Abrangentes
Esta aula integra todos os módulos em um processo completo de fazer pedido, simulando um cenário de negócio do mundo real.
(3) Resultado
Com a integração por Alice completa, o OrderFlow agora tem uma capacidade de negócio totalmente em ciclo fechado—desde login do usuário até fazer pedidos, consultas e cancelamentos, cada etapa é protegida por validação, transações, permissões e tratamento de exceções.
3. Integração do Modelo de Dados
(1) Modelo ER Completo
erDiagram
PRODUCT ||--o{ ORDER_ITEM : "included in"
ORDER ||--o{ ORDER_ITEM : "contains"
USER ||--o{ ORDER : "places"
PRODUCT {
bigint id PK
varchar name
decimal price
int stock
varchar sku
}
ORDER {
bigint id PK
bigint user_id FK
varchar status
decimal total_amount
timestamp created_at
}
ORDER_ITEM {
bigint id PK
bigint order_id FK
bigint product_id FK
int quantity
decimal unit_price
}
USER {
bigint id PK
varchar username
varchar password
varchar email
varchar role
}
(1) ▶ Exemplo: Entidade User
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String role;
protected User() {}
public User(String username, String password, String email, String role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
// getters
}
Saída:
// Execução bem-sucedida
4. Integração do Processo de Pedidos
(1) Linha do Tempo Completa de Pedidos
sequenceDiagram
participant C as Client
participant Ctrl as OrderController
participant Val as Validation
participant Svc as OrderService
participant Tx as Transaction
participant Repo as Repository
C->>Ctrl: POST /api/v1/orders
Ctrl->>Val: @Valid CreateOrderRequest
alt Validação falhou
Val-->>C: 400 VALIDATION_ERROR
end
Ctrl->>Svc: createOrder(request)
Svc->>Tx: Begin Transaction
Svc->>Repo: findProductById
alt Produto não encontrado
Svc-->>C: 404 RESOURCE_NOT_FOUND
end
Svc->>Repo: deductStock (verificar quantidade)
alt Estoque insuficiente
Tx->>Tx: Rollback
Svc-->>C: 409 INSUFFICIENT_STOCK
end
Svc->>Repo: save Order + OrderItems
Tx->>Tx: Commit
Svc-->>Ctrl: Order created
Ctrl-->>C: 201 Created
(1) ▶ Exemplo: OrderController Completo
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request,
@AuthenticationPrincipal UserDetails userDetails) {
OrderResponse order = orderService.createOrder(
request, userDetails.getUsername());
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id, authentication)")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrder(id));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
orderService.cancelOrder(id);
return ResponseEntity.noContent().build();
}
}
Saída:
// Execução bem-sucedida
5. Integração de Transações na Camada de Serviço
(1) ▶ Exemplo: Implementação Completa do OrderService
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final UserRepository userRepository;
public OrderService(OrderRepository orderRepository,
ProductRepository productRepository,
UserRepository userRepository) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.userRepository = userRepository;
}
@Transactional(rollbackFor = Exception.class)
public OrderResponse createOrder(CreateOrderRequest request, String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User", 0L));
Product product = productRepository.findById(request.productId())
.orElseThrow(() -> new ResourceNotFoundException("Product", request.productId()));
if (product.getStock() < request.quantity()) {
throw new InsufficientStockException(
product.getId(), product.getStock(), request.quantity());
}
product.deductStock(request.quantity());
Order order = new Order(user, product, request.quantity());
Order saved = orderRepository.save(order);
return OrderResponse.from(saved);
}
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Long orderId) {
Order order = orderRepository.findByIdWithItems(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
if (!"PENDING".equals(order.getStatus())) {
throw new OrderStateException(orderId, order.getStatus(), "CANCELLED");
}
order.getItems().forEach(item ->
item.getProduct().addStock(item.getQuantity()));
order.setStatus("CANCELLED");
}
@Transactional(readOnly = true)
public OrderResponse getOrder(Long orderId) {
Order order = orderRepository.findByIdWithItems(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
return OrderResponse.from(order);
}
}
Saída:
// Execução bem-sucedida
6. Integrando DTOs com Validação
(1) ▶ Exemplo: DTOs de Requisição/Resposta
// DTO de requisição com validação
public record CreateOrderRequest(
@NotNull(message = "Product ID is required")
Long productId,
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
Integer quantity
) {}
// DTO de resposta
public record OrderResponse(
Long id,
String username,
String status,
BigDecimal totalAmount,
List<OrderItemResponse> items,
Instant createdAt
) {
public static OrderResponse from(Order order) {
List<OrderItemResponse> items = order.getItems().stream()
.map(item -> new OrderItemResponse(
item.getId(),
item.getProduct().getName(),
item.getQuantity(),
item.getUnitPrice(),
item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()))
))
.toList();
return new OrderResponse(
order.getId(),
order.getUser().getUsername(),
order.getStatus(),
order.getTotalAmount(),
items,
order.getCreatedAt()
);
}
}
public record OrderItemResponse(
Long id,
String productName,
Integer quantity,
BigDecimal unitPrice,
BigDecimal subtotal
) {}
Saída:
// Execução bem-sucedida
7. Integração de Segurança
(1) ▶ Exemplo: UserDetailsService Personalizado
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username));
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
// OrderSecurity helper para expressões SpEL
@Component("orderSecurity")
public class OrderSecurity {
private final OrderRepository orderRepository;
public OrderSecurity(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public boolean isOwner(Long orderId, Authentication auth) {
String username = auth.getName();
return orderRepository.findById(orderId)
.map(order -> order.getUser().getUsername().equals(username))
.orElse(false);
}
}
Saída:
// Execução bem-sucedida
8. Exemplo Abrangente: Teste de Processo de Negócio de Ponta a Ponta
# 1. Semear dados de produto
curl -u alice:admin123 -X POST http://localhost:8080/api/v1/products \
-H "Content-Type: application/json" \
-d '{"name":"Laptop Pro","price":1299.99,"stock":50,"sku":"LAP-0001"}'
# 2. Criar pedido como cliente
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":3}'
# 3. Ver próprio pedido (cliente bob)
curl -u bob:pass123 http://localhost:8080/api/v1/orders/1
# 4. Tentar quantidade inválida -> 400
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":0}'
# 5. Tentar pedir mais que o estoque -> 409
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{"productId":1,"quantity":999}'
# 6. Cancelar pedido como admin
curl -u alice:admin123 -X DELETE http://localhost:8080/api/v1/orders/1
| Cenário de Teste | Resultado Esperado | Pontos de Verificação |
|---|---|---|
| Criar Pedido | 201 Created | Garantia de Transação: Pedidos e Estoque São Atualizados Simultaneamente |
| Ver Meus Pedidos | 200 OK | Controle de Acesso: Usuários só podem ver seus próprios pedidos |
| Contagem Inválida | 400 VALIDATION_ERROR | Bean Validation Habilitado |
| Estoque Insuficiente | 409 INSUFFICIENT_STOCK | Exceção de Negócio → Tratamento Global de Exceções |
| Cancelar Pedido | 204 No Content | Transação: Restauração de Estoque |
| Acesso Não Autenticado | 401 Unauthorized | Spring Security Habilitado |
❓ Perguntas Frequentes
logging.level.org.springframework.security=DEBUG para visualizar os logs de decisão de acesso. Você também pode adicionar logs nos métodos Bean referenciados na expressão SpEL.@WithMockUser(roles="ADMIN") Simula o papel ADMIN.📖 Resumo
- Integrar JPA, Services, Validation, Exceptions e Security para construir um fluxo de negócio completo
- Processo de Pedidos: Validar entrada → Verificar estoque → Deduzir estoque → Criar pedido → Commit da transação
- Responsabilidades claras para cada módulo: Controller (validação/autorização) → Service (lógica de negócio/transações) → Repository (dados)
- Personalizar UserDetailsService para substituir usuários em memória; OrderSecurity helper para verificações de permissão em nível de método
- Teste de ponta a ponta cobre fluxo normal + verificação de falha + exceções de negócio + negações de permissão
📝 Exercícios
-
Exercício Básico (Dificuldade: ⭐): Complete toda a integração de código desta aula e garanta que os processos de ponta a ponta para fazer, consultar e cancelar pedidos passem nos testes curl.
-
Exercício Avançado (Dificuldade ⭐⭐): Adicione um módulo de gerenciamento de produtos (ProductController + ProductService) que permite que usuários com o papel ADMIN criem, atualizem e excluam produtos, enquanto usuários comuns só podem consultá-los. Inclua validação e tratamento de exceções abrangentes.
-
Desafio (Dificuldade: ⭐⭐⭐): Use
@SpringBootTest+@WithMockUserpara escrever testes de integração que cubram cenários como pedido bem-sucedido, estoque insuficiente e negação de permissão, alcançando uma taxa de cobertura de teste de pelo menos 80% para o processo principal.



