404 Not Found

404 Not Found


nginx

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


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

100%
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

JAVA
@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:

TEXT
// Execução bem-sucedida

4. Integração do Processo de Pedidos

(1) Linha do Tempo Completa de Pedidos

100%
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

JAVA
@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:

TEXT
// Execução bem-sucedida

5. Integração de Transações na Camada de Serviço

(1) ▶ Exemplo: Implementação Completa do OrderService

JAVA
@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:

TEXT
// Execução bem-sucedida

6. Integrando DTOs com Validação

(1) ▶ Exemplo: DTOs de Requisição/Resposta

JAVA
// 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:

TEXT
// Execução bem-sucedida

7. Integração de Segurança

(1) ▶ Exemplo: UserDetailsService Personalizado

JAVA
@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:

TEXT
// Execução bem-sucedida

8. Exemplo Abrangente: Teste de Processo de Negócio de Ponta a Ponta

BASH
# 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

P Qual é a sequência correta de chamadas em camadas entre módulos?
R Controller → Service → Repository. O Controller trata validação de parâmetros e verificações de autorização, o Service trata lógica de negócio e transações, e o Repository trata acesso a dados. Não faça chamadas entre camadas.
P Como o tratamento de exceções e as transações trabalham juntos?
R Exceções de negócio são lançadas no Service e tratadas uniformemente pelo GlobalExceptionHandler. Quando uma transação detecta uma exceção, ela reverte automaticamente. Tenha cuidado para não capturar uma exceção no Service e não relançá-la; caso contrário, a transação não reverterá.
P Como depurar expressões SpEL no @PreAuthorize?
R Defina 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.
P Como testar a configuração de segurança?
R Use as anotações @SpringBootTest e @WithMockUser para simular um usuário autenticado. @WithMockUser(roles="ADMIN") Simula o papel ADMIN.
P Quais cenários o teste de ponta a ponta deve cobrir?
R No mínimo, deve cobrir: fluxo normal, falhas de validação, exceções de negócio (estoque insuficiente), negações de permissão (pedidos feitos por outra pessoa), e acesso não autenticado. Verifique os códigos de status e formatos de resposta para cada cenário.
P O projeto pode ser implantado após a Fase 2 estar completa?
R A funcionalidade principal está completa, mas ainda não é suficiente. Estão faltando: autenticação JWT (Fase 3), cache (Fase 3), monitoramento (Fase 4), conteinerização (Fase 4) e CI/CD (Fase 5). Recomendamos lançar o projeto apenas após todas as fases estarem completas.

📖 Resumo


📝 Exercícios

  1. 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.

  2. 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.

  3. Desafio (Dificuldade: ⭐⭐⭐): Use @SpringBootTest + @WithMockUser para 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%