Solução 06 - Services e Regras de Negócio 🧠
Navegação
← Exercício 06 | Próxima Solução →
🟢 Respostas Fáceis
1. Lógica fora do Controller
Resposta 1
Por que não colocar lógica no Controller:
**❌ Problemas da Lógica no Controller:**
- **Responsabilidade única violada**: Controller vira "faz-tudo"
- **Reutilização impossível**: Lógica presa a uma rota específica
- **Testes complexos**: Precisa mockar HTTP para testar regra de negócio
- **Manutenção difícil**: Mudança de regra afeta estrutura da API
**✅ Controller deve apenas:**
```javascript
// ✅ Controller limpo e focado
async function criarUsuario(req, res) {
try {
const usuario = await usuarioService.criar(req.body);
return res.status(201).json(usuario);
} catch (error) {
return handleError(error, res);
}
}
```
**🏗️ Separação de Responsabilidades:**
```mermaid
graph TD
A[Controller] --> B[Receber Requisição]
A --> C[Validar Entrada]
A --> D[Chamar Service]
A --> E[Formatar Resposta]
F[Service] --> G[Regras de Negócio]
F --> H[Validações Complexas]
F --> I[Orquestração]
F --> J[Transformações]
style A fill:#e3f2fd
style F fill:#fff3e0
```
2. Tarefas da Camada Service
Resposta 2
3 Exemplos de Tarefas do Service:
1. **Validações Complexas de Negócio**:
```javascript
// Regra: Cliente VIP tem desconto especial
validarDescontoVIP(cliente, valorPedido) {
return cliente.tipo === 'VIP' && valorPedido > 1000;
}
```
2. **Cálculos e Transformações**:
```javascript
// Calcular valor final com impostos e descontos
calcularValorFinal(itens, descontos, regiao) {
const subtotal = itens.reduce((acc, item) => acc + item.total, 0);
const desconto = this.aplicarDescontos(subtotal, descontos);
const impostos = this.calcularImpostos(subtotal, regiao);
return subtotal - desconto + impostos;
}
```
3. **Orquestração de Múltiplos Repositories**:
```javascript
// Coordena operações em diferentes entidades
async transferirSaldo(origemId, destinoId, valor) {
await this.contaRepository.debitar(origemId, valor);
await this.contaRepository.creditar(destinoId, valor);
await this.transacaoRepository.registrar(origemId, destinoId, valor);
}
```
🟡 Respostas Médias
3. Tratamento de Erros no Service
Resposta 3
Por que Service lança erro em vez de retornar Status Code:
**🎯 Separação de Responsabilidades:**
- **Service**: Responsável por **lógica de negócio**
- **Controller**: Responsável por **protocolo HTTP**
```javascript
// ❌ Service não deve conhecer HTTP
class UsuarioService {
async buscar(id) {
const usuario = await this.repository.buscarPorId(id);
if (!usuario) {
return { status: 404, message: 'Not found' }; // ❌ ERRADO!
}
return usuario;
}
}
// ✅ Service foca na regra, Controller no HTTP
class UsuarioService {
async buscar(id) {
const usuario = await this.repository.buscarPorId(id);
if (!usuario) {
throw new UsuarioNaoEncontradoError(`Usuário ${id} não existe`);
}
return usuario;
}
}
class UsuarioController {
async buscar(req, res) {
try {
const usuario = await this.service.buscar(req.params.id);
return res.status(200).json(usuario);
} catch (error) {
if (error instanceof UsuarioNaoEncontradoError) {
return res.status(404).json({ erro: error.message });
}
return res.status(500).json({ erro: 'Erro interno' });
}
}
}
```
**🔄 Benefícios:**
- **Reutilização**: Service usado em Web, CLI, Jobs sem mudanças
- **Testabilidade**: Testa regra de negócio sem HTTP
- **Flexibilidade**: Mesmo erro pode virar diferentes status codes
4. Reutilização do EmailService
Resposta 4
Controllers que usariam EmailService:
**1. AuthController** - Autenticação:
```javascript
class AuthController {
async registrar(req, res) {
const usuario = await this.usuarioService.criar(req.body);
// Enviar e-mail de boas-vindas
await this.emailService.enviarBoasVindas(usuario.email, usuario.nome);
return res.status(201).json(usuario);
}
async resetarSenha(req, res) {
const { email } = req.body;
const token = await this.authService.gerarTokenReset(email);
// Enviar e-mail com link de reset
await this.emailService.enviarResetSenha(email, token);
return res.status(200).json({ sucesso: true });
}
}
```
**2. PedidoController** - E-commerce:
```javascript
class PedidoController {
async finalizar(req, res) {
const pedido = await this.pedidoService.finalizar(req.body);
// Enviar confirmação de pedido
await this.emailService.enviarConfirmacaoPedido(
pedido.cliente.email,
pedido
);
return res.status(201).json(pedido);
}
async atualizar(req, res) {
const pedido = await this.pedidoService.atualizarStatus(req.params.id, req.body);
if (pedido.status === 'enviado') {
// Enviar código de rastreamento
await this.emailService.enviarCodigoRastreamento(
pedido.cliente.email,
pedido.codigoRastreamento
);
}
return res.status(200).json(pedido);
}
}
```
🔴 Resposta Desafio
5. PedidoService.finalizar() - Pseudocódigo
Resposta 5
Implementação Completa com Validações:
```javascript
class PedidoService {
async finalizar(pedidoId) {
// 1. BUSCAR E VALIDAR PEDIDO
const pedido = await this.pedidoRepository.buscarPorId(pedidoId);
if (!pedido) {
throw new PedidoNaoEncontradoError(`Pedido ${pedidoId} não existe`);
}
if (pedido.status !== 'carrinho') {
throw new PedidoJaFinalizadoError(
`Pedido ${pedidoId} já foi finalizado (status: ${pedido.status})`
);
}
// 2. VALIDAR CLIENTE
const cliente = await this.clienteRepository.buscarPorId(pedido.clienteId);
if (!cliente.ativo) {
throw new ClienteInativoError(`Cliente ${cliente.id} está inativo`);
}
// 3. VALIDAR ESTOQUE E COLETAR ITENS
const itensValidados = [];
for (const item of pedido.itens) {
const produto = await this.produtoRepository.buscarPorId(item.produtoId);
if (!produto) {
throw new ProdutoNaoEncontradoError(`Produto ${item.produtoId} não existe`);
}
if (produto.estoque < item.quantidade) {
throw new EstoqueInsuficienteError(
`Produto ${produto.nome} tem apenas ${produto.estoque} unidades, solicitado: ${item.quantidade}`
);
}
itensValidados.push({
...item,
produto,
valorUnitario: produto.preco
});
}
// 4. CALCULAR TOTAIS
const subtotal = itensValidados.reduce(
(acc, item) => acc + (item.valorUnitario * item.quantidade), 0
);
const desconto = await this.calculadoraDescontoService.calcular(cliente, itensValidados);
const frete = await this.calculadoraFreteService.calcular(cliente.endereco, itensValidados);
const impostos = this.calculadoraImpostosService.calcular(subtotal, cliente.endereco.estado);
const valorTotal = subtotal - desconto + frete + impostos;
// 5. VALIDAR LIMITE DE CRÉDITO
if (cliente.limiteCreditoDisponivel < valorTotal) {
throw new LimiteCreditoExcedidoError(
`Limite insuficiente. Disponível: R$ ${cliente.limiteCreditoDisponivel}, Necessário: R$ ${valorTotal}`
);
}
// 6. INICIAR TRANSAÇÃO (atomicidade)
const transaction = await this.database.beginTransaction();
try {
// 7. RESERVAR ESTOQUE
for (const item of itensValidados) {
await this.produtoRepository.reduzirEstoque(
item.produtoId,
item.quantidade,
transaction
);
}
// 8. ATUALIZAR PEDIDO
const pedidoFinalizado = await this.pedidoRepository.finalizar({
id: pedidoId,
status: 'processando',
subtotal,
desconto,
frete,
impostos,
valorTotal,
dataFinalizacao: new Date(),
itens: itensValidados
}, transaction);
// 9. REGISTRAR MOVIMENTAÇÃO FINANCEIRA
await this.financeiroRepository.registrarDebito(
cliente.id,
valorTotal,
`Pedido ${pedidoId}`,
transaction
);
// 10. COMMIT DA TRANSAÇÃO
await transaction.commit();
// 11. PROCESSOS ASSÍNCRONOS (PÓS-COMMIT)
this.eventBus.publish('pedido.finalizado', {
pedidoId: pedidoFinalizado.id,
clienteId: cliente.id,
valorTotal
});
// 12. RETORNAR DTO PARA O CONTROLLER
return {
id: pedidoFinalizado.id,
numero: pedidoFinalizado.numero,
status: pedidoFinalizado.status,
cliente: {
nome: cliente.nome,
email: cliente.email
},
itens: itensValidados.map(item => ({
produto: item.produto.nome,
quantidade: item.quantidade,
valorUnitario: item.valorUnitario,
subtotal: item.valorUnitario * item.quantidade
})),
resumoFinanceiro: {
subtotal,
desconto,
frete,
impostos,
valorTotal
},
dataFinalizacao: pedidoFinalizado.dataFinalizacao,
estimativaEntrega: await this.logisticaService.calcularEstimativaEntrega(
cliente.endereco
)
};
} catch (error) {
await transaction.rollback();
throw error;
}
}
// TRATAMENTO DO ERRO "Produto Sem Estoque"
async tratarEstoqueInsuficiente(pedidoId, produtoId) {
// Opções disponíveis:
// 1. Remover item do pedido
await this.removerItemDoPedido(pedidoId, produtoId);
// 2. Reduzir quantidade para disponível
const estoque = await this.produtoRepository.obterEstoque(produtoId);
if (estoque > 0) {
await this.atualizarQuantidadeItem(pedidoId, produtoId, estoque);
}
// 3. Sugerir produtos similares
const similares = await this.produtoRepository.buscarSimilares(produtoId);
return {
acao: 'estoque_insuficiente',
produtoOriginal: produtoId,
estoqueDisponivel: estoque,
produtosSimilares: similares
};
}
}
```
**🎯 DTO de Retorno para o Controller:**
```typescript
interface PedidoFinalizadoDTO {
id: number;
numero: string;
status: 'processando';
cliente: {
nome: string;
email: string;
};
itens: ItemPedidoDTO[];
resumoFinanceiro: {
subtotal: number;
desconto: number;
frete: number;
impostos: number;
valorTotal: number;
};
dataFinalizacao: Date;
estimativaEntrega: Date;
}
```
**🚨 Tratamento de "Produto Sem Estoque":**
```mermaid
flowchart TD
A[Validar Estoque] --> B{Estoque >= Quantidade?}
B -->|Sim| C[Continuar Processamento]
B -->|Não| D[EstoqueInsuficienteError]
D --> E[Controller Captura Erro]
E --> F[Status 409 Conflict]
F --> G[Retornar Opções ao Cliente]
G --> H[1. Remover Item]
G --> I[2. Reduzir Quantidade]
G --> J[3. Produtos Similares]
G --> K[4. Aguardar Reestoque]
style D fill:#ff6b6b
style F fill:#ffa726
```
!!! tip "Dicas para Próximos Estudos" - Implemente Domain Driven Design (DDD) para regras complexas - Use Events para desacoplar processos assíncronos - Pratique Transaction Scripts vs Domain Model patterns - Configure Circuit Breakers para serviços externos
Navegação