💎 Módulo 02: Regras de Negócio e o Papel Estoquista

Com a persistência pronta (Módulo 01), este módulo implementa a regra de negócio central do projeto: registrar uma movimentação de estoque validando saldo, e o novo papel ROLE_ESTOQUISTA que autoriza quem pode fazer isso.


### Aula 2.1: A Exceção de Negócio Nova

Conceito-Chave: “Saída maior que o saldo disponível” não é um erro técnico (like ResourceNotFoundException) — é uma regra de negócio violada. Segue o mesmo padrão de exceção customizada + handler central já usado em _01.

Código: exception/EstoqueInsuficienteException.java

package br.com.aula.gestaodeestoques.exception;

public class EstoqueInsuficienteException extends RuntimeException {
    public EstoqueInsuficienteException(String message) {
        super(message);
    }
}

Código: exception/GlobalExceptionHandler.java (handler novo)

@ExceptionHandler(EstoqueInsuficienteException.class)
public ResponseEntity<ErrorResponseDTO> handleEstoqueInsuficiente(EstoqueInsuficienteException ex, HttpServletRequest request) {
    ErrorResponseDTO error = new ErrorResponseDTO(
        Instant.now(), HttpStatus.BAD_REQUEST.value(), "Estoque Insuficiente",
        ex.getMessage(), request.getRequestURI());
    return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}

⚠️ Achado real durante a implementação: ao testar @PreAuthorize num slice @WebMvcTest, descobrimos que GlobalExceptionHandler não tinha handler para AccessDeniedException (lançada quando o @PreAuthorize nega acesso). Em produção isso não é visível porque o ExceptionTranslationFilter da cadeia de segurança intercepta antes e converte para 403 automaticamente — mas se essa exceção escapar do filtro por qualquer motivo, caía no handler genérico (Exception.class) e virava um 500 opaco em vez de um 403 claro. Adicionamos um handler dedicado como rede de segurança (ver Módulo 03 para o teste que revelou isso).


### Aula 2.2: DTOs e Mapper

Código: dto/MovimentacaoEstoqueFormDTO.java

package br.com.aula.gestaodeestoques.dto;

import br.com.aula.gestaodeestoques.model.TipoMovimentacao;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record MovimentacaoEstoqueFormDTO(
    @NotNull(message = "O produto é obrigatório.")
    Integer produtoId,

    @NotNull(message = "O tipo de movimentação é obrigatório.")
    TipoMovimentacao tipo,

    @NotNull(message = "A quantidade é obrigatória.")
    @Positive(message = "A quantidade deve ser maior que zero.")
    Integer quantidade
) {}

Código: dto/MovimentacaoEstoqueDTO.java

package br.com.aula.gestaodeestoques.dto;

import br.com.aula.gestaodeestoques.model.TipoMovimentacao;
import java.time.LocalDateTime;

// DTO para exibir uma movimentação, enriquecido com os nomes de produto e usuário.
public record MovimentacaoEstoqueDTO(
    Long id,
    Integer produtoId,
    String nomeProduto,
    TipoMovimentacao tipo,
    int quantidade,
    LocalDateTime dataHora,
    String loginUsuario
) {}

Código: mapper/MovimentacaoEstoqueMapper.java

@Component
public class MovimentacaoEstoqueMapper {
    public MovimentacaoEstoqueDTO toDTO(MovimentacaoEstoque movimentacao, Produto produto, Usuario usuario) {
        return new MovimentacaoEstoqueDTO(
                movimentacao.id(),
                movimentacao.produtoId(),
                produto != null ? produto.nome() : "N/A",
                movimentacao.tipo(),
                movimentacao.quantidade(),
                movimentacao.dataHora(),
                usuario != null ? usuario.login() : "N/A"
        );
    }
}

### Aula 2.3: O Serviço — Validar Saldo e Atualizar Atomicamente

Conceito-Chave: esta é a regra de negócio central do projeto. Uma SAIDA só é aceita se quantidade solicitada <= quantidade em estoque. As duas escritas (atualizar Produto.quantidade + inserir MovimentacaoEstoque) acontecem na mesma transação — ver o diagrama de sequência do Módulo 00.

Código: service/impl/MovimentacaoEstoqueServiceImpl.java

@Service
public class MovimentacaoEstoqueServiceImpl implements MovimentacaoEstoqueService {

    private final MovimentacaoEstoqueRepository movimentacaoEstoqueRepository;
    private final ProdutoRepository produtoRepository;
    private final UsuarioRepository usuarioRepository;
    private final MovimentacaoEstoqueMapper mapper;

    // ... construtor com injeção via construtor, mesmo padrão de ProdutoServiceImpl ...

    @Override
    @Transactional
    public MovimentacaoEstoqueDTO registrar(MovimentacaoEstoqueFormDTO formDTO) {
        Produto produto = produtoRepository.findById(formDTO.produtoId())
                .orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado com o ID: " + formDTO.produtoId()));

        Usuario usuarioLogado = usuarioAutenticado();

        int novaQuantidade;
        if (formDTO.tipo() == TipoMovimentacao.SAIDA) {
            if (formDTO.quantidade() > produto.quantidade()) {
                throw new EstoqueInsuficienteException(
                        "Estoque insuficiente para o produto '" + produto.nome() + "'. Disponível: "
                                + produto.quantidade() + ", solicitado: " + formDTO.quantidade() + ".");
            }
            novaQuantidade = produto.quantidade() - formDTO.quantidade();
        } else {
            novaQuantidade = produto.quantidade() + formDTO.quantidade();
        }

        // Atualiza a quantidade do produto e registra a movimentação na mesma transação:
        // se uma das duas falhar, a outra é revertida (nenhuma fica "órfã").
        Produto produtoAtualizado = new Produto(produto.id(), produto.nome(), novaQuantidade, produto.preco(),
                produto.estoqueMinimo(), produto.categoriaId(), produto.fornecedorId());
        produtoRepository.save(produtoAtualizado);

        MovimentacaoEstoque movimentacao = new MovimentacaoEstoque(null, produto.id(), formDTO.tipo(),
                formDTO.quantidade(), LocalDateTime.now(), usuarioLogado.id());
        MovimentacaoEstoque salva = movimentacaoEstoqueRepository.save(movimentacao);

        return mapper.toDTO(salva, produtoAtualizado, usuarioLogado);
    }

    private Usuario usuarioAutenticado() {
        String login = SecurityContextHolder.getContext().getAuthentication().getName();
        return usuarioRepository.findByLogin(login)
                .orElseThrow(() -> new ResourceNotFoundException("Usuário autenticado não encontrado: " + login));
    }

    // findAll()/findByProdutoId() seguem o mesmo padrão de busca em lote (evitar N+1)
    // já usado em ProdutoServiceImpl.findAll() — ver código-fonte completo no projeto.
}

💡 De onde vem usuarioLogado? SecurityContextHolder.getContext().getAuthentication().getName() devolve o login do usuário autenticado no token JWT da requisição atual — o mesmo mecanismo que o JwtAuthenticationFilter de _01 já popula em SecurityContextHolder a cada requisição. Não é um parâmetro que a SPA envia; o backend descobre “quem está fazendo isso” a partir do próprio token, o que impede um usuário de registrar uma movimentação “em nome de outro”.


### Aula 2.4: O Papel ROLE_ESTOQUISTA

Ação: papéis são seed data, não schema — ROLE_ESTOQUISTA é criado pelo DataSeeder, exatamente como ROLE_ADMIN/ROLE_USER já eram em _01.

Código: config/DataSeeder.java (trechos novos)

// 1. Papéis
Papel estoquistaPapel = papelRepository.save(new Papel(null, "ROLE_ESTOQUISTA"));

// 2. Usuário de exemplo
Usuario estoquista = new Usuario(null, "estoquista", passwordEncoder.encode("estoquista123"), true);
Usuario savedEstoquista = usuarioRepository.save(estoquista);

// 3. Associação
usuarioRepository.adicionarPapel(savedEstoquista.id(), estoquistaPapel.id());

O seeder também popula Produto.estoqueMinimo para cada produto de exemplo e insere algumas MovimentacaoEstoque de histórico inicial — incluindo, de propósito, um produto com quantidade abaixo do estoqueMinimo, para o alerta (RF11, Módulo 03) já aparecer populado na primeira execução.


Conclusão do Módulo 02

A regra de negócio está completa: validação de saldo, transação atômica, e o papel ROLE_ESTOQUISTA pronto para autorizar quem pode movimentar estoque. No próximo módulo, expomos tudo isso como API REST — os controllers, a autorização por papel nos endpoints, e os endpoints de busca (RF09) e alerta (RF11).