📚 Módulo 04: Backend - Camada de Serviços e Transações

✅ Pré-Requisitos deste Módulo

Confirme antes de começar:

./mvnw clean compile   # deve retornar BUILD SUCCESS

Você deve ter concluído o Módulo 03 (todos os DTOs, Mappers e GlobalExceptionHandler criados).

Em Engenharia de Software, os controladores (Controllers) devem ser finos, encarregando-se apenas da recepção de dados HTTP e de seu respectivo retorno. O cérebro da nossa aplicação, contendo as regras e a orquestração do negócio, reside na Camada de Serviço (Service Layer).

Neste módulo, implementaremos os serviços de Produto e Pedido, focando na lógica de faturamento de pedidos e no uso da anotação @Transactional para garantir a consistência transacional do estoque.


🏛️ 1. O Padrão Service com Interfaces

A boa prática sugere o uso de Interfaces para definir os contratos dos nossos serviços. Isso traz dois benefícios essenciais:

  1. Desacoplamento: A API conhece apenas o contrato, facilitando a substituição da implementação concreta no futuro.
  2. Facilidade de Testes Unitários: Permite criar “mocks” (dublês de teste) da interface de forma simples para testar outras partes do código isoladamente.

Criaremos a estrutura de serviços no pacote br.com.tecloja.api.service e suas implementações no subpacote br.com.tecloja.api.service.impl.


📦 2. Serviço de Produtos

1. ProdutoService.java

package br.com.tecloja.api.service;

import br.com.tecloja.api.dto.ProdutoDTO;
import br.com.tecloja.api.dto.ProdutoFormDTO;
import java.util.List;

public interface ProdutoService {
    List<ProdutoDTO> listarTodos();
    ProdutoDTO buscarPorId(Long id);
    List<ProdutoDTO> buscarPorCategoria(Long categoriaId);
    List<ProdutoDTO> pesquisarPorNome(String busca);
    ProdutoDTO criar(ProdutoFormDTO form);
    ProdutoDTO atualizar(Long id, ProdutoFormDTO form);
    void deletar(Long id);
}

2. ProdutoServiceImpl.java

package br.com.tecloja.api.service.impl;

import br.com.tecloja.api.dto.ProdutoDTO;
import br.com.tecloja.api.dto.ProdutoFormDTO;
import br.com.tecloja.api.exception.ResourceNotFoundException;
import br.com.tecloja.api.mapper.ProdutoMapper;
import br.com.tecloja.api.model.Categoria;
import br.com.tecloja.api.model.Produto;
import br.com.tecloja.api.repository.CategoriaRepository;
import br.com.tecloja.api.repository.ProdutoRepository;
import br.com.tecloja.api.service.ProdutoService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class ProdutoServiceImpl implements ProdutoService {

    private final ProdutoRepository produtoRepository;
    private final CategoriaRepository categoriaRepository;

    public ProdutoServiceImpl(ProdutoRepository produtoRepository, CategoriaRepository categoriaRepository) {
        this.produtoRepository = produtoRepository;
        this.categoriaRepository = categoriaRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProdutoDTO> listarTodos() {
        return produtoRepository.findAll().stream()
            .filter(Produto::isAtivo)
            .map(ProdutoMapper::toDTO)
            .collect(Collectors.toList());
    }

    @Override
    @Transactional(readOnly = true)
    public ProdutoDTO buscarPorId(Long id) {
        Produto produto = produtoRepository.findById(id)
            .filter(Produto::isAtivo)
            .orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado ou inativo com o ID: " + id));
        return ProdutoMapper.toDTO(produto);
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProdutoDTO> buscarPorCategoria(Long categoriaId) {
        return produtoRepository.findByCategoriaId(categoriaId).stream()
            .filter(Produto::isAtivo)
            .map(ProdutoMapper::toDTO)
            .collect(Collectors.toList());
    }

    @Override
    @Transactional(readOnly = true)
    public List<ProdutoDTO> pesquisarPorNome(String busca) {
        return produtoRepository.pesquisarPorNome(busca).stream()
            .filter(Produto::isAtivo)
            .map(ProdutoMapper::toDTO)
            .collect(Collectors.toList());
    }

    @Override
    @Transactional
    public ProdutoDTO criar(ProdutoFormDTO form) {
        Categoria categoria = categoriaRepository.findById(form.categoriaId())
            .orElseThrow(() -> new ResourceNotFoundException("Categoria não encontrada com o ID: " + form.categoriaId()));
        Produto produto = ProdutoMapper.toEntity(form, categoria);
        Produto salvo = produtoRepository.save(produto);
        return ProdutoMapper.toDTO(salvo);
    }

    @Override
    @Transactional
    public ProdutoDTO atualizar(Long id, ProdutoFormDTO form) {
        Produto produto = produtoRepository.findById(id)
            .filter(Produto::isAtivo)
            .orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado ou inativo com o ID: " + id));
        
        Categoria categoria = categoriaRepository.findById(form.categoriaId())
            .orElseThrow(() -> new ResourceNotFoundException("Categoria não encontrada com o ID: " + form.categoriaId()));

        produto.setNome(form.nome());
        produto.setDescricao(form.descricao());
        produto.setPreco(form.preco());
        produto.setEstoque(form.estoque());
        produto.setCategoria(categoria);

        Produto atualizado = produtoRepository.save(produto);
        return ProdutoMapper.toDTO(atualizado);
    }

    @Override
    @Transactional
    public void deletar(Long id) {
        Produto produto = produtoRepository.findById(id)
            .filter(Produto::isAtivo)
            .orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado ou inativo com o ID: " + id));
        
        // Em vez de deletar fisicamente, inativamos o produto (Soft Delete)
        produto.setAtivo(false);
        produtoRepository.save(produto);
    }
}

[!NOTE] Boas Práticas de Mercado — Por que usamos Soft Delete (Exclusão Lógica)?

Em sistemas comerciais reais (especialmente e-commerces), a exclusão física (Hard Delete) de registros é considerada uma prática extremamente arriscada. Se você excluir fisicamente um produto do banco de dados, todos os pedidos históricos de venda realizados no passado que referenciavam esse produto perderão a integridade referencial, gerando falhas graves no banco ou quebrando relatórios de faturamento.

Ao adotar o Soft Delete, nós apenas alteramos a flag ativo para false. O produto deixa de ser exibido na vitrine e nas pesquisas, mas permanece intacto no histórico do banco, preservando com absoluta segurança a rastreabilidade e integridade contábil das vendas passadas.


🛒 3. Serviço de Pedidos e Controle de Estoque Transacional

O checkout é a regra de negócio mais complexa e importante da TecLoja. Ao faturar um pedido, o sistema precisa:

  1. Validar se o cliente existe.
  2. Para cada item do pedido, validar se o produto existe no catálogo.
  3. Verificar a disponibilidade de estoque: se a quantidade solicitada for maior que o saldo em estoque, o faturamento deve ser cancelado.
  4. Reduzir a quantidade vendida do estoque do produto.
  5. Gravar os dados históricos do produto (preço unitário cobrado no instante da compra) no ItemPedido.
  6. Persistir as informações estruturadas.

🧠 Por que utilizar @Transactional?

A anotação @Transactional do Spring informa que todos os comandos executados no método fazem parte de uma única transação de banco de dados (Princípio ACID).

Se o cliente tentar comprar 3 itens e um deles falhar por estoque insuficiente no meio do processo, toda a operação sofrerá um “Rollback”. Nenhum produto terá seu estoque debitado e nenhuma linha de pedido será escrita no banco, mantendo o banco de dados livre de inconsistências.

sequenceDiagram
    participant API as PedidoController
    participant Service as PedidoServiceImpl
    participant DB as Banco (PostgreSQL)

    API->>Service: realizarPedido()
    Note over Service: @Transactional inicia
    Service->>DB: BEGIN
    loop Verificando Itens
        Service->>DB: Buscar Estoque
        alt Estoque Insuficiente
            Service-->>API: throws BusinessException
            Service->>DB: ROLLBACK (Automático)
        else Estoque Ok
            Service->>DB: UPDATE (Baixa de Estoque)
        end
    end
    Service->>DB: INSERT Pedido / Itens
    Service->>DB: COMMIT (Automático)
    Service-->>API: PedidoDTO (Sucesso)

1. PedidoService.java

package br.com.tecloja.api.service;

import br.com.tecloja.api.dto.PedidoDTO;
import br.com.tecloja.api.dto.PedidoFormDTO;
import java.util.List;

public interface PedidoService {
    PedidoDTO realizarPedido(PedidoFormDTO form);
    List<PedidoDTO> listarPedidosPorCliente(Long clienteId);
}

2. PedidoServiceImpl.java

package br.com.tecloja.api.service.impl;

import br.com.tecloja.api.dto.ItemPedidoFormDTO;
import br.com.tecloja.api.dto.PedidoDTO;
import br.com.tecloja.api.dto.PedidoFormDTO;
import br.com.tecloja.api.exception.BusinessException;
import br.com.tecloja.api.exception.ResourceNotFoundException;
import br.com.tecloja.api.mapper.PedidoMapper;
import br.com.tecloja.api.model.*;
import br.com.tecloja.api.repository.*;
import br.com.tecloja.api.service.PedidoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
public class PedidoServiceImpl implements PedidoService {

    private final PedidoRepository pedidoRepository;
    private final ClienteRepository clienteRepository;
    private final ProdutoRepository produtoRepository;

    public PedidoServiceImpl(PedidoRepository pedidoRepository, ClienteRepository clienteRepository,
                             ProdutoRepository produtoRepository) {
        this.pedidoRepository = pedidoRepository;
        this.clienteRepository = clienteRepository;
        this.produtoRepository = produtoRepository;
    }

    @Override
    @Transactional // Inicia uma transação ativa. Qualquer erro causará rollback automático do estoque!
    public PedidoDTO realizarPedido(PedidoFormDTO form) {
        log.info("Iniciando faturamento de pedido para o cliente ID: {}", form.clienteId());

        // 1. Validar Cliente
        Cliente cliente = clienteRepository.findById(form.clienteId())
            .orElseThrow(() -> new ResourceNotFoundException("Cliente não encontrado com o ID: " + form.clienteId()));

        // 2. Instanciar Novo Pedido
        Pedido pedido = new Pedido();
        pedido.setCliente(cliente);
        pedido.setStatus("PAGO"); // Venda simplificada com pagamento imediato aprovado
        pedido.setDataPedido(LocalDateTime.now());

        // 3. Processar Itens e Dar Baixa de Estoque
        for (ItemPedidoFormDTO itemForm : form.itens()) {
            Produto produto = produtoRepository.findById(itemForm.produtoId())
                .orElseThrow(() -> new ResourceNotFoundException("Produto não encontrado com o ID: " + itemForm.produtoId()));

            // Regra de Negócio: produto inativo não pode ser vendido (Soft Delete)
            if (!produto.isAtivo()) {
                log.warn("Falha no checkout: tentativa de comprar produto inativo '{}' (ID: {}).", produto.getNome(), produto.getId());
                throw new BusinessException(String.format(
                    "O produto '%s' não está mais disponível para venda no catálogo.",
                    produto.getNome()
                ));
            }

            // Regra Crítica de Banco de Dados/Negócio: Validação de Estoque
            if (produto.getEstoque() < itemForm.quantidade()) {
                log.warn("Falha no checkout: estoque insuficiente para o produto '{}' (ID: {}). Solicitado: {}, Disponível: {}", 
                    produto.getNome(), produto.getId(), itemForm.quantidade(), produto.getEstoque());
                throw new BusinessException(String.format(
                    "Estoque insuficiente para o produto '%s'. Disponível: %d, Solicitado: %d.",
                    produto.getNome(), produto.getEstoque(), itemForm.quantidade()
                ));
            }

            // Reduzir estoque físico do produto e salvar
            produto.setEstoque(produto.getEstoque() - itemForm.quantidade());
            produtoRepository.save(produto);

            // Criar ItemPedido com cópia de preço unitário histórico
            ItemPedido item = new ItemPedido();
            item.setProduto(produto);
            item.setQuantidade(itemForm.quantidade());
            item.setPrecoUnitario(produto.getPreco()); // Copia o preço ATUAL do produto

            // Vincula o item ao pedido de forma bidirecional
            pedido.adicionarItem(item);
        }

        // 4. Salvar Pedido e Itens em cascata
        Pedido pedidoSalvo = pedidoRepository.save(pedido);
        log.info("Pedido ID: {} faturado com sucesso para o cliente ID: {}.", pedidoSalvo.getId(), cliente.getId());

        // 5. Retornar DTO faturado
        return PedidoMapper.toDTO(pedidoSalvo);
    }

    @Override
    @Transactional(readOnly = true)
    public List<PedidoDTO> listarPedidosPorCliente(Long clienteId) {
        // Valida se o cliente existe antes
        if (!clienteRepository.existsById(clienteId)) {
            throw new ResourceNotFoundException("Cliente não encontrado com o ID: " + clienteId);
        }
        
        return pedidoRepository.findPedidosCompletosPorCliente(clienteId).stream()
            .map(PedidoMapper::toDTO)
            .collect(Collectors.toList());
    }
}

[!NOTE] Boas Práticas de Mercado — Por que usamos SLF4J / Logging Estruturado?

Em sistemas reais em produção, a ausência de logs torna impossível depurar erros ou rastrear o comportamento dos usuários em tempo real (observabilidade). Usar comandos simples como System.out.println é considerado uma má prática gravíssima em Java corporativo, pois essas impressões travam a thread de execução (I/O síncrono) e não possuem níveis de severidade.

A biblioteca SLF4J (Simple Logging Facade for Java) com o Logback (facilmente integrado via Spring Boot) permite registrar eventos com diferentes níveis de severidade:

Além disso, note a sintaxe parametrizada log.info("Mensagem: {}", valor). Essa estrutura evita a concatenação de strings desnecessária em memória antes do log ser de fato filtrado, otimizando drasticamente o desempenho de APIs de alto tráfego.


🔍 Checkpoint

Após criar os serviços, compile e execute os testes unitários:

./mvnw clean test

Resultado esperado: BUILD SUCCESS com todos os testes passando (Tests run: X, Failures: 0, Errors: 0).

Se ainda não criou o arquivo de teste, pode verificar a compilação simples com ./mvnw clean compile. Os testes completos serão adicionados no Módulo 09.

⚠️ Erros Comuns

Sintoma Causa Solução
No qualifying bean of type 'ProdutoService' Interface sem implementação detectada Confirme que ProdutoServiceImpl tem a anotação @Service
Could not commit JPA transaction Exceção lançada dentro de @Transactional Comportamento correto — o Spring faz rollback automaticamente. Verifique a mensagem de erro no JSON de resposta
LazyInitializationException no Mapper getCategoria() chamado fora de sessão ativa Confirme que listarTodos() tem @Transactional(readOnly = true)
Estoque negativo após pedido @Transactional ausente no realizarPedido Confirme a anotação no método realizarPedido em PedidoServiceImpl

🏁 Conclusão e Próximos Passos

A inteligência da TecLoja está concluída! Ao programarmos as regras transacionais de faturamento de pedidos, os alunos passam a entender a importância do controle de concorrência e integridade referencial em banco de dados utilizando JPA e transações ativas.

No Módulo 05, exporemos essa lógica de negócio para a internet. Desenvolveremos controladores REST (@RestController), configuraremos as permissões de CORS para receber requisições do frontend hospedado na Netlify e blindaremos a API com segurança avançada por meio do Spring Security e JWT.


Voltar para o Sumário