Lição 3: cliente
Nesta lição, você vai implementar o subdomínio CRM e seu agregado cliente, que referencia o município criado na lição 2. Ao final, o projeto corresponde ao diretório exemplo do repositório de D5.
Como na lição 2, esta lição explica apenas o que muda em relação às anteriores:
- um módulo de subdomínio que passa a depender de outro subdomínio;
- um identificador artificial, gerado pelo banco de dados na inclusão;
- um objeto de valor formado por vários atributos, o endereço;
- uma regra que depende do estado de outro agregado: o cliente não pode ser vinculado a um município extinto;
- uma projeção que traz dados de dois outros agregados;
- o município passa a ser referenciado e, por isso, não pode mais ser excluído enquanto houver clientes nele.
Projeto Maven
O subdomínio CRM corresponde ao módulo exemplo-dominio-crm, que o gerador já criou na lição 1, junto com a sua entrada no pom.xml do projeto raiz e a dependência do módulo de aplicação nele.
O cliente referencia o município e, para validar essa referência, seu serviço usa o repositório de município. Por isso, além do domínio compartilhado, o módulo exemplo-dominio-crm passa a depender de exemplo-dominio-geografia. Acrescente a dependência ao pom.xml do módulo:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>exemplo-pai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>exemplo-dominio-crm</artifactId>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-compartilhado</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>exemplo-dominio-geografia</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sauloaraujo</groupId>
<artifactId>d5-framework-dominio-teste</artifactId>
<type>pom</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Um módulo de subdomínio pode depender de outro, desde que as dependências entre eles não formem um ciclo — por isso Geografia nunca dependerá do CRM. Já o código IBGE do município não vai para o domínio compartilhado, ainda que passe a ser usado pelos dois subdomínios: ele pertence a Geografia, e o compartilhado só contém conceitos sem um subdomínio dono; veja Dependências entre módulos de domínio.
Com essa mudança, as dependências entre os módulos passam a ser as do diagrama a seguir, que, como o da lição 1, omite o frontend:
graph TD
subgraph Framework
FD[d5-framework-dominio]
FA[d5-framework-aplicacao]
FI[d5-framework-infraestrutura]
FU[d5-framework-ui-backend]
FS[d5-framework-springdoc]
end
subgraph exemplo
DC[exemplo-dominio-compartilhado]
DG[exemplo-dominio-geografia]
DR[exemplo-dominio-crm]
AP[exemplo-aplicacao]
IN[exemplo-infraestrutura]
UI[exemplo-ui-backend]
end
DC --> FD
DG --> DC
DR --> DC
DR --> DG
AP --> FA
AP --> DG
AP --> DR
IN --> FI
IN --> AP
UI --> FU
UI --> AP
UI --> DC
UI -.-> FS
UI -.-> IN
Os pom.xml da infraestrutura e do backend não mudam: o novo módulo de subdomínio chega a eles transitivamente pelo módulo de aplicação.
Cliente
Um cliente possui CPF único, nome e endereço. Diferente da unidade federativa e do município, cujo identificador é um código definido pelo IBGE, o identificador do cliente é artificial: ele é gerado pelo banco de dados quando o cliente é incluído. Seguindo as convenções, o pacote do agregado é com.example.exemplo.dominio.crm.cliente. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.crm.cliente;
Domínio
Identificador
O identificador do cliente não tem formato definido pelo domínio — é um número gerado pelo banco de dados, que o domínio trata apenas como texto. Por isso, ele herda de ObjetoValorStringAbstrato, e não de ObjetoValorRegexAbstrato, exigindo apenas que não esteja em branco. Como os códigos IBGE, implementa IIdentificador.
package com.example.exemplo.dominio.crm.cliente;
import org.apache.commons.lang3.Validate;
import dev.sauloaraujo.d5.framework.dominio.IIdentificador;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorStringAbstrato;
public final class ClienteIdentificador extends ObjetoValorStringAbstrato implements IIdentificador {
public ClienteIdentificador(String valor) {
super(Validate.notBlank(valor, "Identificador do cliente em branco"));
}
}
CPF e CEP
O CPF e o CEP são formados por onze e oito dígitos, respectivamente, e, como a sigla e o código IBGE, herdam de ObjetoValorRegexAbstrato. Para manter o exemplo simples, o CPF valida apenas o formato, e não os dígitos verificadores: 11111111111, usado nos testes e nas capturas desta lição, é aceito, embora não seja um CPF válido.
package com.example.exemplo.dominio.crm.cliente;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;
public final class Cpf extends ObjetoValorRegexAbstrato {
public static final int TAMANHO = 11;
private static final String FORMATO = "\\d{" + TAMANHO + "}";
private static final String MENSAGEM = "O CPF deve conter " + TAMANHO + " dígitos";
public Cpf(String valor) {
super(valor, FORMATO, MENSAGEM);
}
}
package com.example.exemplo.dominio.crm.cliente;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;
public final class EnderecoCep extends ObjetoValorRegexAbstrato {
public static final int TAMANHO = 8;
private static final String FORMATO = "\\d{" + TAMANHO + "}";
private static final String MENSAGEM = "O CEP do endereço deve conter " + TAMANHO + " dígitos";
public EnderecoCep(String valor) {
super(valor, FORMATO, MENSAGEM);
}
}
Endereço
O endereço é um objeto de valor formado por vários atributos — logradouro, número, complemento opcional, bairro, CEP e município —, por isso implementa IObjetoValor diretamente e redefine equals e hashCode a partir de todos eles. Assim como o município referencia a unidade federativa apenas pelo identificador, o endereço referencia o município apenas pelo seu código IBGE.
package com.example.exemplo.dominio.crm.cliente;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.Validate;
import org.jspecify.annotations.Nullable;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import dev.sauloaraujo.d5.framework.dominio.IObjetoValor;
public final class Endereco implements IObjetoValor {
private final String logradouro;
private final String numero;
private final Optional<String> complemento;
private final String bairro;
private final EnderecoCep cep;
private final MunicipioCodigoIbge municipio;
public Endereco(String logradouro, String numero, Optional<String> complemento, String bairro, EnderecoCep cep,
MunicipioCodigoIbge municipio) {
this.logradouro = Validate.notBlank(logradouro, "Logradouro do endereço em branco");
this.numero = Validate.notBlank(numero, "Número do endereço em branco");
this.complemento = Objects.requireNonNull(complemento);
this.bairro = Validate.notBlank(bairro, "Bairro do endereço em branco");
this.cep = Objects.requireNonNull(cep);
this.municipio = Objects.requireNonNull(municipio);
}
public String getLogradouro() {
return logradouro;
}
public String getNumero() {
return numero;
}
public Optional<String> getComplemento() {
return complemento;
}
public String getBairro() {
return bairro;
}
public EnderecoCep getCep() {
return cep;
}
public MunicipioCodigoIbge getMunicipio() {
return municipio;
}
@Override
public boolean equals(@Nullable Object objeto) {
if (this == objeto) {
return true;
}
if (objeto == null || getClass() != objeto.getClass()) {
return false;
}
var outro = (Endereco) objeto;
return logradouro.equals(outro.logradouro) && numero.equals(outro.numero)
&& complemento.equals(outro.complemento) && bairro.equals(outro.bairro) && cep.equals(outro.cep)
&& municipio.equals(outro.municipio);
}
@Override
public int hashCode() {
return Objects.hash(logradouro, numero, complemento, bairro, cep, municipio);
}
}
Agregado
Como o identificador só existe depois que o cliente é incluído, o construtor o recebe envolto em um Optional: vazio na inclusão e presente nas demais operações.
package com.example.exemplo.dominio.crm.cliente;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.Validate;
import dev.sauloaraujo.d5.framework.dominio.IAgregado;
public final class Cliente implements IAgregado<ClienteIdentificador> {
private final Optional<ClienteIdentificador> identificador;
private final Cpf cpf;
private final String nome;
private final Endereco endereco;
public Cliente(Optional<ClienteIdentificador> identificador, Cpf cpf, String nome, Endereco endereco) {
this.identificador = Objects.requireNonNull(identificador);
this.cpf = Objects.requireNonNull(cpf);
this.nome = Validate.notBlank(nome, "Nome do cliente em branco");
this.endereco = Objects.requireNonNull(endereco);
}
@Override
public Optional<ClienteIdentificador> getIdentificador() {
return identificador;
}
public Cpf getCpf() {
return cpf;
}
public String getNome() {
return nome;
}
public Endereco getEndereco() {
return endereco;
}
}
Repositório
O repositório expõe existePorCpf, que, como existePorSigla na unidade federativa, ignora o identificador do próprio cliente na verificação. Crie o pacote spi e seu package-info.java, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.crm.cliente.spi;
package com.example.exemplo.dominio.crm.cliente.spi;
import java.util.Optional;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.ClienteIdentificador;
import com.example.exemplo.dominio.crm.cliente.Cpf;
import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;
public interface IClienteRepositorio extends IAgregadoRepositorio<ClienteIdentificador, Cliente> {
boolean existePorCpf(Cpf cpf, Optional<ClienteIdentificador> identificadorIgnorado);
}
Serviço
O serviço recebe, além do repositório de cliente, o repositório de município, do subdomínio Geografia. Em validarInclusaoAlteracao, ele exige que o CPF seja único e, validando o estado da aplicação, que o município do endereço não esteja extinto, reutilizando o modelo de mensagem de Municipio. Para isso, busca o município com buscarPorIdentificador, que já lança AgregadoInexistenteExcecao quando ele não existe — dispensando uma verificação de existência à parte. Como a regra vale também para a alteração, um cliente de um município extinto só pode ser alterado se o seu endereço passar a apontar para outro município.
Como o identificador do cliente é gerado pelo banco de dados, o serviço sobrescreve também validarInclusao, validando o argumento para rejeitar um cliente que chegue à inclusão já com identificador. Como AgregadoServicoAbstrato chama validarInclusao antes de verificar a inexistência do identificador, a inclusão com identificador responde 422 mesmo que o identificador pertença a outro cliente — o problema não é o conflito com o cliente existente, mas o identificador ter sido informado.
package com.example.exemplo.dominio.crm.cliente;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.Validate;
import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.crm.cliente.spi.IClienteRepositorio;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.spi.IMunicipioRepositorio;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
public final class ClienteServico
extends AgregadoServicoAbstrato<ClienteIdentificador, Cliente, AgregadoAutorizacoes<Cliente>> {
public static final String IDENTIFICADOR_INEXISTENTE_MODELO = "Não existe cliente com o identificador %s";
public static final String IDENTIFICADOR_EXISTENTE_MODELO = "Já existe um cliente com o identificador %s";
public static final String CPF_EXISTENTE_MODELO = "Já existe um cliente com o CPF %s";
public static final String IDENTIFICADOR_INFORMADO = "O identificador do cliente é gerado na inclusão e não deve ser informado";
private final IClienteRepositorio repositorio;
private final IMunicipioRepositorio municipioRepositorio;
public ClienteServico(ISegurancaServico segurancaServico, IClienteRepositorio repositorio,
IMunicipioRepositorio municipioRepositorio) {
super(segurancaServico, repositorio, IDENTIFICADOR_EXISTENTE_MODELO, IDENTIFICADOR_INEXISTENTE_MODELO);
this.repositorio = Objects.requireNonNull(repositorio);
this.municipioRepositorio = Objects.requireNonNull(municipioRepositorio);
}
@Override
public AgregadoAutorizacoes<Cliente> criarAutorizacoes(Optional<ClienteIdentificador> identificador) {
Objects.requireNonNull(identificador);
var administrador = getSegurancaServico().possuiAutorizacao(Autorizacao.ADMINISTRADOR.name());
return new AgregadoAutorizacoes<>(administrador, true, administrador, administrador);
}
@Override
protected void validarInclusao(Cliente agregado) {
Objects.requireNonNull(agregado);
Validate.isTrue(agregado.getIdentificador().isEmpty(), IDENTIFICADOR_INFORMADO);
}
@Override
protected void validarInclusaoAlteracao(Cliente agregado) {
Objects.requireNonNull(agregado);
validarInexistencia(cpf -> repositorio.existePorCpf(cpf, agregado.getIdentificador()), agregado.getCpf(),
CPF_EXISTENTE_MODELO);
var municipio = agregado.getEndereco().getMunicipio();
Validate.validState(!municipioRepositorio.buscarPorIdentificador(municipio).isExtinto(),
Municipio.EXTINTO_MODELO, municipio);
}
}
Testes
Como todo módulo de subdomínio, exemplo-dominio-crm tem a sua suíte do Cucumber:
package com.example.exemplo.dominio.crm;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectDirectories;
import org.junit.platform.suite.api.Suite;
import io.cucumber.junit.platform.engine.Constants;
@Suite(failIfNoTests = false)
@IncludeEngines("cucumber")
@ConfigurationParameter(key = Constants.GLUE_PROPERTY_NAME, value = "com.example.exemplo.dominio.crm")
@SelectDirectories("src/test/resources")
public class CucumberTests {
}
Escreva os cenários cobrindo inclusão, busca, alteração e exclusão de um cliente, incluindo as regras de autorização, de unicidade do CPF e de existência e extinção do município:
# language: pt
Funcionalidade: Gerenciar clientes
Para manter o cadastro de clientes da aplicação
Como usuário do sistema
Quero incluir, buscar, alterar e excluir clientes,
respeitando as regras de unicidade de CPF, de município e de autorização
Contexto:
Dado que não existe nenhum cliente cadastrado
E que existe o município de código IBGE "3550308"
Cenário: Incluir um cliente
Dado que sou um administrador do cadastro de clientes
Quando eu incluo o cliente de CPF "11111111111", nome "Maria" e município "3550308"
Então o cliente é incluído com sucesso
E o cliente de CPF "11111111111" passa a existir
Cenário: Não é possível incluir um cliente sem autorização de administrador
Dado que sou um usuário sem autorização para administrar clientes
Quando eu tento incluir o cliente de CPF "11111111111", nome "Maria" e município "3550308"
Então a inclusão do cliente é rejeitada por falta de autorização
Cenário: Não é possível incluir um cliente com identificador informado
Dado que sou um administrador do cadastro de clientes
Quando eu tento incluir o cliente de identificador "999", CPF "11111111111", nome "Maria" e município "3550308"
Então a inclusão é rejeitada porque o identificador do cliente é gerado na inclusão
Cenário: Não é possível incluir um cliente com o identificador de outro cliente
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "22222222222" e nome "Ana"
Quando eu tento incluir o cliente de identificador "1", CPF "11111111111", nome "Maria" e município "3550308"
Então a inclusão é rejeitada porque o identificador do cliente é gerado na inclusão
Cenário: Não é possível incluir um cliente com CPF já existente
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
Quando eu tento incluir o cliente de CPF "11111111111", nome "Ana" e município "3550308"
Então a inclusão é rejeitada porque já existe um cliente com o CPF "11111111111"
Cenário: Não é possível incluir um cliente em um município inexistente
Dado que sou um administrador do cadastro de clientes
Quando eu tento incluir o cliente de CPF "11111111111", nome "Maria" e município "3304557"
Então a inclusão é rejeitada porque não existe município com o identificador "3304557"
Cenário: Não é possível incluir um cliente em um município extinto
Dado que sou um administrador do cadastro de clientes
E que existe o município extinto de código IBGE "3304557"
Quando eu tento incluir o cliente de CPF "11111111111", nome "Maria" e município "3304557"
Então a inclusão é rejeitada porque o município "3304557" está extinto
Cenário: Buscar um cliente existente
Dado que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
E que sou um usuário sem autorização para administrar clientes
Quando eu busco o cliente de identificador "1"
Então o cliente encontrado tem CPF "11111111111" e nome "Maria"
Cenário: Não é possível buscar um cliente inexistente
Dado que sou um usuário sem autorização para administrar clientes
Quando eu tento buscar o cliente de identificador "1"
Então a busca é rejeitada porque não existe cliente com o identificador "1"
Cenário: Alterar um cliente
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
Quando eu altero o cliente de identificador "1" para CPF "11111111111" e nome "Maria Silva"
Então o cliente é alterado com sucesso
E o cliente de identificador "1" passa a ter CPF "11111111111" e nome "Maria Silva"
Cenário: Não é possível alterar um cliente sem autorização de administrador
Dado que sou um usuário sem autorização para administrar clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
Quando eu tento alterar o cliente de identificador "1" para CPF "11111111111" e nome "Maria Silva"
Então a alteração do cliente é rejeitada por falta de autorização
Cenário: Não é possível alterar um cliente inexistente
Dado que sou um administrador do cadastro de clientes
Quando eu tento alterar o cliente de identificador "1" para CPF "11111111111" e nome "Maria"
Então a alteração é rejeitada porque não existe cliente com o identificador "1"
Cenário: Não é possível alterar um cliente para o CPF de outro
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
E que existe um cliente com identificador "2", CPF "22222222222" e nome "Ana"
Quando eu tento alterar o cliente de identificador "2" para CPF "11111111111" e nome "Ana"
Então a alteração é rejeitada porque já existe um cliente com o CPF "11111111111"
Cenário: Não é possível alterar um cliente de um município extinto sem trocar o município
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
E que o município de código IBGE "3550308" foi extinto
Quando eu tento alterar o cliente de identificador "1" para CPF "11111111111" e nome "Maria Silva"
Então a alteração é rejeitada porque o município "3550308" está extinto
Cenário: Alterar um cliente de um município extinto para outro município
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
E que o município de código IBGE "3550308" foi extinto
E que existe o município de código IBGE "3509502"
Quando eu altero o cliente de identificador "1" para CPF "11111111111", nome "Maria" e município "3509502"
Então o cliente é alterado com sucesso
E o cliente de identificador "1" passa a ter o município "3509502"
Cenário: Não é possível alterar um cliente para um município inexistente
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
Quando eu tento alterar o cliente de identificador "1" para CPF "11111111111", nome "Maria" e município "3304557"
Então a alteração é rejeitada porque não existe município com o identificador "3304557"
Cenário: Excluir um cliente
Dado que sou um administrador do cadastro de clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
Quando eu excluo o cliente de identificador "1"
Então o cliente é excluído com sucesso
E o cliente de identificador "1" deixa de existir
Cenário: Não é possível excluir um cliente sem autorização de administrador
Dado que sou um usuário sem autorização para administrar clientes
E que existe um cliente com identificador "1", CPF "11111111111" e nome "Maria"
Quando eu tento excluir o cliente de identificador "1"
Então a exclusão do cliente é rejeitada por falta de autorização
Cenário: Não é possível excluir um cliente inexistente
Dado que sou um administrador do cadastro de clientes
Quando eu tento excluir o cliente de identificador "1"
Então a exclusão é rejeitada porque não existe cliente com o identificador "1"
As step definitions dublam também o repositório de município, que simula os municípios através de um mapa em memória. Como o identificador é gerado pelo banco de dados, o repositório de cliente simulado gera identificadores sequenciais na inclusão.
package com.example.exemplo.dominio.crm.cliente;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.crm.cliente.spi.IClienteRepositorio;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.municipio.MunicipioServico;
import com.example.exemplo.dominio.geografia.municipio.spi.IMunicipioRepositorio;
import dev.sauloaraujo.d5.framework.dominio.AgregadoExistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.dominio.spi.NaoAutorizadoExcecao;
import io.cucumber.java.pt.Dado;
import io.cucumber.java.pt.Entao;
import io.cucumber.java.pt.Quando;
public final class ClienteStepDefinitions {
private static final String MUNICIPIO_PADRAO = "3550308";
private final Map<String, Cliente> clientes = new HashMap<>();
private final Map<String, Municipio> municipios = new HashMap<>();
private final ISegurancaServico segurancaServico = mock(ISegurancaServico.class);
private final IClienteRepositorio repositorio = mock(IClienteRepositorio.class);
private final IMunicipioRepositorio municipioRepositorio = mock(IMunicipioRepositorio.class);
private final ClienteServico servico = new ClienteServico(segurancaServico, repositorio, municipioRepositorio);
private int proximoIdentificador = 1;
private Cliente resultadoInclusao;
private Cliente resultadoBusca;
private RuntimeException excecaoCapturada;
public ClienteStepDefinitions() {
when(municipioRepositorio.buscarPorIdentificador(any())).thenAnswer(chamada -> {
MunicipioCodigoIbge codigoIbge = chamada.getArgument(0);
var municipio = municipios.get(codigoIbge.toString());
if (municipio == null) {
throw new AgregadoInexistenteExcecao(
String.format(MunicipioServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge));
}
return municipio;
});
when(repositorio.existePorIdentificador(any()))
.thenAnswer(chamada -> clientes.containsKey(chave(chamada.getArgument(0))));
when(repositorio.buscarPorIdentificador(any())).thenAnswer(chamada -> {
ClienteIdentificador identificador = chamada.getArgument(0);
var cliente = clientes.get(chave(identificador));
if (cliente == null) {
throw new AgregadoInexistenteExcecao(
String.format(ClienteServico.IDENTIFICADOR_INEXISTENTE_MODELO, identificador));
}
return cliente;
});
when(repositorio.existePorCpf(any(), any())).thenAnswer(chamada -> {
Cpf cpf = chamada.getArgument(0);
Optional<ClienteIdentificador> identificadorIgnorado = chamada.getArgument(1);
return clientes.values().stream().anyMatch(
candidato -> candidato.getCpf().equals(cpf) && !foiIgnorado(candidato, identificadorIgnorado));
});
when(repositorio.incluir(any())).thenAnswer(chamada -> {
Cliente cliente = chamada.getArgument(0);
var identificador = new ClienteIdentificador(String.valueOf(proximoIdentificador++));
var incluido = new Cliente(Optional.of(identificador), cliente.getCpf(), cliente.getNome(),
cliente.getEndereco());
clientes.put(chave(identificador), incluido);
return incluido;
});
doAnswer(chamada -> {
Cliente cliente = chamada.getArgument(0);
clientes.put(chave(cliente.getIdentificador().orElseThrow()), cliente);
return null;
}).when(repositorio).alterar(any());
doAnswer(chamada -> {
ClienteIdentificador identificador = chamada.getArgument(0);
clientes.remove(chave(identificador));
return null;
}).when(repositorio).excluir(any());
}
private static String chave(ClienteIdentificador identificador) {
return identificador.toString();
}
private static boolean foiIgnorado(Cliente candidato, Optional<ClienteIdentificador> identificadorIgnorado) {
return identificadorIgnorado.isPresent() && candidato.getIdentificador().equals(identificadorIgnorado);
}
private static Cliente criar(Optional<String> identificador, String cpf, String nome, String municipio) {
var endereco = new Endereco("Avenida Paulista", "1000", Optional.empty(), "Bela Vista",
new EnderecoCep("01310100"), new MunicipioCodigoIbge(municipio));
return new Cliente(identificador.map(ClienteIdentificador::new), new Cpf(cpf), nome, endereco);
}
private void adicionarMunicipio(String codigoIbge, boolean extinto) {
var municipio = new Municipio(new MunicipioCodigoIbge(codigoIbge), "Município " + codigoIbge, Optional.empty());
municipios.put(codigoIbge, extinto ? municipio.extinguir() : municipio);
}
@Dado("que não existe nenhum cliente cadastrado")
public void que_nao_existe_nenhum_cliente_cadastrado() {
clientes.clear();
municipios.clear();
}
@Dado("que existe o município de código IBGE {string}")
public void que_existe_o_municipio(String codigoIbge) {
adicionarMunicipio(codigoIbge, false);
}
@Dado("que existe o município extinto de código IBGE {string}")
public void que_existe_o_municipio_extinto(String codigoIbge) {
adicionarMunicipio(codigoIbge, true);
}
@Dado("que o município de código IBGE {string} foi extinto")
public void que_o_municipio_foi_extinto(String codigoIbge) {
municipios.put(codigoIbge, municipios.get(codigoIbge).extinguir());
}
@Dado("que sou um administrador do cadastro de clientes")
public void que_sou_um_administrador_do_cadastro_de_clientes() {
when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(true);
}
@Dado("que sou um usuário sem autorização para administrar clientes")
public void que_sou_um_usuario_sem_autorizacao_para_administrar_clientes() {
when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(false);
}
@Dado("que existe um cliente com identificador {string}, CPF {string} e nome {string}")
public void que_existe_um_cliente(String identificador, String cpf, String nome) {
clientes.put(identificador, criar(Optional.of(identificador), cpf, nome, MUNICIPIO_PADRAO));
}
@Quando("eu incluo o cliente de CPF {string}, nome {string} e município {string}")
public void eu_incluo_o_cliente(String cpf, String nome, String municipio) {
incluir(cpf, nome, municipio);
}
@Quando("eu tento incluir o cliente de CPF {string}, nome {string} e município {string}")
public void eu_tento_incluir_o_cliente(String cpf, String nome, String municipio) {
incluir(cpf, nome, municipio);
}
@Quando("eu tento incluir o cliente de identificador {string}, CPF {string}, nome {string} e município {string}")
public void eu_tento_incluir_o_cliente_com_identificador(String identificador, String cpf, String nome,
String municipio) {
incluir(Optional.of(identificador), cpf, nome, municipio);
}
private void incluir(String cpf, String nome, String municipio) {
incluir(Optional.empty(), cpf, nome, municipio);
}
private void incluir(Optional<String> identificador, String cpf, String nome, String municipio) {
try {
resultadoInclusao = servico.incluir(criar(identificador, cpf, nome, municipio));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu busco o cliente de identificador {string}")
public void eu_busco_o_cliente(String identificador) {
buscar(identificador);
}
@Quando("eu tento buscar o cliente de identificador {string}")
public void eu_tento_buscar_o_cliente(String identificador) {
buscar(identificador);
}
private void buscar(String identificador) {
try {
resultadoBusca = servico.buscar(new ClienteIdentificador(identificador));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu altero o cliente de identificador {string} para CPF {string} e nome {string}")
public void eu_altero_o_cliente(String identificador, String cpf, String nome) {
alterar(identificador, cpf, nome);
}
@Quando("eu tento alterar o cliente de identificador {string} para CPF {string} e nome {string}")
public void eu_tento_alterar_o_cliente(String identificador, String cpf, String nome) {
alterar(identificador, cpf, nome);
}
@Quando("eu altero o cliente de identificador {string} para CPF {string}, nome {string} e município {string}")
public void eu_altero_o_cliente_e_o_municipio(String identificador, String cpf, String nome, String municipio) {
alterar(identificador, cpf, nome, municipio);
}
@Quando("eu tento alterar o cliente de identificador {string} para CPF {string}, nome {string} e município {string}")
public void eu_tento_alterar_o_cliente_e_o_municipio(String identificador, String cpf, String nome,
String municipio) {
alterar(identificador, cpf, nome, municipio);
}
private void alterar(String identificador, String cpf, String nome) {
alterar(identificador, cpf, nome, MUNICIPIO_PADRAO);
}
private void alterar(String identificador, String cpf, String nome, String municipio) {
try {
servico.alterar(criar(Optional.of(identificador), cpf, nome, municipio));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu excluo o cliente de identificador {string}")
public void eu_excluo_o_cliente(String identificador) {
excluir(identificador);
}
@Quando("eu tento excluir o cliente de identificador {string}")
public void eu_tento_excluir_o_cliente(String identificador) {
excluir(identificador);
}
private void excluir(String identificador) {
try {
servico.excluir(new ClienteIdentificador(identificador));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Entao("o cliente é incluído com sucesso")
public void o_cliente_e_incluido_com_sucesso() {
assertNull(excecaoCapturada);
assertNotNull(resultadoInclusao);
assertTrue(resultadoInclusao.getIdentificador().isPresent());
}
@Entao("o cliente de CPF {string} passa a existir")
public void o_cliente_passa_a_existir(String cpf) {
assertTrue(clientes.values().stream().anyMatch(cliente -> cliente.getCpf().equals(new Cpf(cpf))));
}
@Entao("a inclusão do cliente é rejeitada por falta de autorização")
public void a_inclusao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a inclusão é rejeitada porque o identificador do cliente é gerado na inclusão")
public void a_inclusao_e_rejeitada_pelo_identificador_informado() {
assertInstanceOf(IllegalArgumentException.class, excecaoCapturada);
assertEquals(ClienteServico.IDENTIFICADOR_INFORMADO, excecaoCapturada.getMessage());
}
@Entao("a inclusão é rejeitada porque já existe um cliente com o CPF {string}")
public void a_inclusao_e_rejeitada_pelo_cpf(String cpf) {
assertMensagemDeCpfExistente(cpf);
}
@Entao("a inclusão é rejeitada porque não existe município com o identificador {string}")
public void a_inclusao_e_rejeitada_por_municipio_inexistente(String codigoIbge) {
assertMensagemDeMunicipioInexistente(codigoIbge);
}
@Entao("a inclusão é rejeitada porque o município {string} está extinto")
public void a_inclusao_e_rejeitada_por_municipio_extinto(String codigoIbge) {
assertMensagemDeMunicipioExtinto(codigoIbge);
}
@Entao("o cliente encontrado tem CPF {string} e nome {string}")
public void o_cliente_encontrado_tem(String cpf, String nome) {
assertEquals(new Cpf(cpf), resultadoBusca.getCpf());
assertEquals(nome, resultadoBusca.getNome());
}
@Entao("a busca é rejeitada porque não existe cliente com o identificador {string}")
public void a_busca_e_rejeitada_por_inexistencia(String identificador) {
assertMensagemDeInexistencia(identificador);
}
@Entao("o cliente é alterado com sucesso")
public void o_cliente_e_alterado_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("o cliente de identificador {string} passa a ter CPF {string} e nome {string}")
public void o_cliente_passa_a_ter(String identificador, String cpf, String nome) {
var cliente = clientes.get(identificador);
assertNotNull(cliente);
assertEquals(new Cpf(cpf), cliente.getCpf());
assertEquals(nome, cliente.getNome());
}
@Entao("o cliente de identificador {string} passa a ter o município {string}")
public void o_cliente_passa_a_ter_o_municipio(String identificador, String municipio) {
var cliente = clientes.get(identificador);
assertNotNull(cliente);
assertEquals(new MunicipioCodigoIbge(municipio), cliente.getEndereco().getMunicipio());
}
@Entao("a alteração do cliente é rejeitada por falta de autorização")
public void a_alteracao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a alteração é rejeitada porque não existe cliente com o identificador {string}")
public void a_alteracao_e_rejeitada_por_inexistencia(String identificador) {
assertMensagemDeInexistencia(identificador);
}
@Entao("a alteração é rejeitada porque já existe um cliente com o CPF {string}")
public void a_alteracao_e_rejeitada_pelo_cpf(String cpf) {
assertMensagemDeCpfExistente(cpf);
}
@Entao("a alteração é rejeitada porque não existe município com o identificador {string}")
public void a_alteracao_e_rejeitada_por_municipio_inexistente(String codigoIbge) {
assertMensagemDeMunicipioInexistente(codigoIbge);
}
@Entao("a alteração é rejeitada porque o município {string} está extinto")
public void a_alteracao_e_rejeitada_por_municipio_extinto(String codigoIbge) {
assertMensagemDeMunicipioExtinto(codigoIbge);
}
@Entao("o cliente é excluído com sucesso")
public void o_cliente_e_excluido_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("o cliente de identificador {string} deixa de existir")
public void o_cliente_deixa_de_existir(String identificador) {
assertFalse(clientes.containsKey(identificador));
}
@Entao("a exclusão do cliente é rejeitada por falta de autorização")
public void a_exclusao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a exclusão é rejeitada porque não existe cliente com o identificador {string}")
public void a_exclusao_e_rejeitada_por_inexistencia(String identificador) {
assertMensagemDeInexistencia(identificador);
}
private void assertMensagemDeCpfExistente(String cpf) {
assertInstanceOf(AgregadoExistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(ClienteServico.CPF_EXISTENTE_MODELO, cpf), excecaoCapturada.getMessage());
}
private void assertMensagemDeInexistencia(String identificador) {
assertInstanceOf(AgregadoInexistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(ClienteServico.IDENTIFICADOR_INEXISTENTE_MODELO, identificador),
excecaoCapturada.getMessage());
}
private void assertMensagemDeMunicipioInexistente(String codigoIbge) {
assertInstanceOf(AgregadoInexistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(MunicipioServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge),
excecaoCapturada.getMessage());
}
private void assertMensagemDeMunicipioExtinto(String codigoIbge) {
assertInstanceOf(IllegalStateException.class, excecaoCapturada);
assertEquals(String.format(Municipio.EXTINTO_MODELO, codigoIbge), excecaoCapturada.getMessage());
}
}
Município referenciado
Agora que clientes referenciam municípios, o município passa por mudança análoga à que a unidade federativa sofreu na lição 2. Acrescente existeReferencia ao repositório de município:
package com.example.exemplo.dominio.geografia.municipio.spi;
import java.util.Optional;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;
public interface IMunicipioRepositorio extends IAgregadoRepositorio<MunicipioCodigoIbge, Municipio> {
boolean existePorNome(String nome, UnidadeFederativaCodigoIbge unidadeFederativa,
Optional<MunicipioCodigoIbge> identificadorIgnorado);
boolean existeReferencia(MunicipioCodigoIbge identificador);
}
Em MunicipioServico, sobrescreva validarExclusao para impedir a exclusão de um município referenciado:
package com.example.exemplo.dominio.geografia.municipio;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.Validate;
import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.geografia.municipio.spi.IMunicipioRepositorio;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
public final class MunicipioServico
extends AgregadoServicoAbstrato<MunicipioCodigoIbge, Municipio, MunicipioAutorizacoes> {
public static final String IDENTIFICADOR_INEXISTENTE_MODELO = "Não existe município com o identificador %s";
public static final String IDENTIFICADOR_EXISTENTE_MODELO = "Já existe um município com o identificador %s";
public static final String NOME_EXISTENTE_MODELO = "Já existe um município com o nome %s na unidade federativa";
public static final String EXTINCAO_INDEVIDA = "O município só pode ser extinto pela ação de extinção";
public static final String REFERENCIADO_MODELO = "O município %s é referenciado e não pode ser excluído";
private final IMunicipioRepositorio repositorio;
private final IUnidadeFederativaRepositorio unidadeFederativaRepositorio;
public MunicipioServico(ISegurancaServico segurancaServico, IMunicipioRepositorio repositorio,
IUnidadeFederativaRepositorio unidadeFederativaRepositorio) {
super(segurancaServico, repositorio, IDENTIFICADOR_EXISTENTE_MODELO, IDENTIFICADOR_INEXISTENTE_MODELO);
this.repositorio = Objects.requireNonNull(repositorio);
this.unidadeFederativaRepositorio = Objects.requireNonNull(unidadeFederativaRepositorio);
}
@Override
public MunicipioAutorizacoes criarAutorizacoes(Optional<MunicipioCodigoIbge> identificador) {
Objects.requireNonNull(identificador);
var administrador = getSegurancaServico().possuiAutorizacao(Autorizacao.ADMINISTRADOR.name());
return new MunicipioAutorizacoes(administrador, true, administrador, administrador, administrador);
}
@Override
protected void validarInclusao(Municipio agregado) {
Objects.requireNonNull(agregado);
Validate.isTrue(!agregado.isExtinto(), EXTINCAO_INDEVIDA);
}
@Override
protected void validarInclusaoAlteracao(Municipio agregado) {
Objects.requireNonNull(agregado);
validarExistencia(unidadeFederativaRepositorio::existePorIdentificador, agregado.getUnidadeFederativa(),
UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO);
validarInexistencia(
nome -> repositorio.existePorNome(nome, agregado.getUnidadeFederativa(), agregado.getIdentificador()),
agregado.getNome(), NOME_EXISTENTE_MODELO);
}
@Override
protected void validarAlteracao(Municipio agregado) {
Objects.requireNonNull(agregado);
Validate.isTrue(!agregado.isExtinto(), EXTINCAO_INDEVIDA);
var identificador = agregado.getIdentificador().orElseThrow();
var municipio = repositorio.buscarPorIdentificador(identificador);
Validate.validState(!municipio.isExtinto(), Municipio.EXTINTO_MODELO, identificador);
}
@Override
protected void validarExclusao(MunicipioCodigoIbge identificador) {
Objects.requireNonNull(identificador);
Validate.validState(!repositorio.existeReferencia(identificador), REFERENCIADO_MODELO, identificador);
}
public void extinguir(MunicipioCodigoIbge identificador) {
Objects.requireNonNull(identificador);
validarAutorizacao(Optional.of(identificador), MunicipioAutorizacoes::getExtinguir);
validarExistencia(identificador);
var municipio = repositorio.buscarPorIdentificador(identificador);
repositorio.alterar(municipio.extinguir());
}
}
Note que o subdomínio Geografia continua sem depender do subdomínio CRM: o serviço de município não sabe que são clientes que o referenciam. Acrescente aos cenários do município a exclusão de um município referenciado e, às step definitions, a simulação dos municípios referenciados e o passo que verifica que o município continua a existir depois da exclusão rejeitada:
# language: pt
Funcionalidade: Gerenciar municípios
Para manter o cadastro de municípios da aplicação
Como usuário do sistema
Quero incluir, buscar, alterar, excluir e extinguir municípios,
respeitando as regras de unidade federativa, unicidade de nome e de autorização
Contexto:
Dado que não existe nenhum município cadastrado
E que existe a unidade federativa de código IBGE "35"
Cenário: Incluir um município
Dado que sou um administrador do cadastro de municípios
Quando eu incluo o município de código IBGE "3550308", nome "São Paulo"
Então o município é incluído com sucesso
E o município de código IBGE "3550308" passa a existir
Cenário: Não é possível incluir um município sem autorização de administrador
Dado que sou um usuário sem autorização para administrar municípios
Quando eu tento incluir o município de código IBGE "3550308", nome "São Paulo"
Então a inclusão do município é rejeitada por falta de autorização
Cenário: Não é possível incluir um município com código IBGE já existente
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento incluir o município de código IBGE "3550308", nome "Santos"
Então a inclusão é rejeitada porque já existe um município com o identificador "3550308"
Cenário: Não é possível incluir um município com nome já existente na mesma unidade federativa
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento incluir o município de código IBGE "3548500", nome "São Paulo"
Então a inclusão é rejeitada porque já existe um município com o nome "São Paulo" na unidade federativa
Cenário: Não é possível incluir um município com nome já existente na mesma unidade federativa, ainda que com outras maiúsculas e minúsculas
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento incluir o município de código IBGE "3548500", nome "SÃO PAULO"
Então a inclusão é rejeitada porque já existe um município com o nome "SÃO PAULO" na unidade federativa
Cenário: Não é possível incluir um município em uma unidade federativa inexistente
Dado que sou um administrador do cadastro de municípios
Quando eu tento incluir o município de código IBGE "3304557", nome "Rio de Janeiro"
Então a inclusão é rejeitada porque não existe unidade federativa com o identificador "33"
Cenário: Não é possível incluir um município já extinto
Dado que sou um administrador do cadastro de municípios
Quando eu tento incluir o município extinto de código IBGE "3550308" e nome "São Paulo"
Então a inclusão é rejeitada porque o município só pode ser extinto pela ação de extinção
Cenário: Não é possível incluir um município já extinto, ainda que com código IBGE já existente
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento incluir o município extinto de código IBGE "3550308" e nome "São Paulo"
Então a inclusão é rejeitada porque o município só pode ser extinto pela ação de extinção
Cenário: Buscar um município existente
Dado que existe um município com código IBGE "3550308", nome "São Paulo"
E que sou um usuário sem autorização para administrar municípios
Quando eu busco o município de código IBGE "3550308"
Então o município encontrado tem nome "São Paulo"
Cenário: Não é possível buscar um município inexistente
Dado que sou um usuário sem autorização para administrar municípios
Quando eu tento buscar o município de código IBGE "3550308"
Então a busca é rejeitada porque não existe município com o identificador "3550308"
Cenário: Alterar um município
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu altero o município de código IBGE "3550308" para nome "Cidade de São Paulo"
Então o município é alterado com sucesso
E o município de código IBGE "3550308" passa a ter nome "Cidade de São Paulo"
Cenário: Alterar um município mantendo seu próprio nome
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu altero o município de código IBGE "3550308" para nome "São Paulo"
Então o município é alterado com sucesso
Cenário: Não é possível alterar um município sem autorização de administrador
Dado que sou um usuário sem autorização para administrar municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento alterar o município de código IBGE "3550308" para nome "Cidade de São Paulo"
Então a alteração do município é rejeitada por falta de autorização
Cenário: Não é possível alterar um município inexistente
Dado que sou um administrador do cadastro de municípios
Quando eu tento alterar o município de código IBGE "3550308" para nome "São Paulo"
Então a alteração é rejeitada porque não existe município com o identificador "3550308"
Cenário: Não é possível alterar um município extinto
Dado que sou um administrador do cadastro de municípios
E que existe um município extinto com código IBGE "3550308", nome "São Paulo"
Quando eu tento alterar o município de código IBGE "3550308" para nome "Cidade de São Paulo"
Então a alteração é rejeitada porque o município "3550308" está extinto
Cenário: Não é possível alterar um município extinto, ainda que para o nome de outro município
Dado que sou um administrador do cadastro de municípios
E que existe um município extinto com código IBGE "3550308", nome "São Paulo"
E que existe um município com código IBGE "3548500", nome "Santos"
Quando eu tento alterar o município de código IBGE "3550308" para nome "Santos"
Então a alteração é rejeitada porque o município "3550308" está extinto
Cenário: Não é possível alterar um município extinto informando-o como extinto
Dado que sou um administrador do cadastro de municípios
E que existe um município extinto com código IBGE "3550308", nome "São Paulo"
Quando eu tento alterar o município de código IBGE "3550308" para extinto com nome "Cidade de São Paulo"
Então a alteração é rejeitada porque o município só pode ser extinto pela ação de extinção
Cenário: Não é possível extinguir um município por meio da alteração
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento alterar o município de código IBGE "3550308" para extinto com nome "São Paulo"
Então a alteração é rejeitada porque o município só pode ser extinto pela ação de extinção
Cenário: Excluir um município
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu excluo o município de código IBGE "3550308"
Então o município é excluído com sucesso
E o município de código IBGE "3550308" deixa de existir
Cenário: Não é possível excluir um município sem autorização de administrador
Dado que sou um usuário sem autorização para administrar municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento excluir o município de código IBGE "3550308"
Então a exclusão do município é rejeitada por falta de autorização
Cenário: Não é possível excluir um município inexistente
Dado que sou um administrador do cadastro de municípios
Quando eu tento excluir o município de código IBGE "3550308"
Então a exclusão é rejeitada porque não existe município com o identificador "3550308"
Cenário: Não é possível excluir um município referenciado
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
E que o município de código IBGE "3550308" é referenciado
Quando eu tento excluir o município de código IBGE "3550308"
Então a exclusão é rejeitada porque o município "3550308" é referenciado
E o município de código IBGE "3550308" continua a existir
Cenário: Extinguir um município
Dado que sou um administrador do cadastro de municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu extingo o município de código IBGE "3550308"
Então o município é extinguido com sucesso
E o município de código IBGE "3550308" passa a estar extinto
Cenário: Não é possível extinguir um município sem autorização de administrador
Dado que sou um usuário sem autorização para administrar municípios
E que existe um município com código IBGE "3550308", nome "São Paulo"
Quando eu tento extinguir o município de código IBGE "3550308"
Então a extinção do município é rejeitada por falta de autorização
Cenário: Não é possível extinguir um município inexistente
Dado que sou um administrador do cadastro de municípios
Quando eu tento extinguir o município de código IBGE "3550308"
Então a extinção é rejeitada porque não existe município com o identificador "3550308"
Cenário: Não é possível extinguir um município já extinto
Dado que sou um administrador do cadastro de municípios
E que existe um município extinto com código IBGE "3550308", nome "São Paulo"
Quando eu tento extinguir o município de código IBGE "3550308"
Então a extinção é rejeitada porque o município "3550308" está extinto
package com.example.exemplo.dominio.geografia.municipio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import com.example.exemplo.dominio.Autorizacao;
import com.example.exemplo.dominio.geografia.municipio.spi.IMunicipioRepositorio;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;
import dev.sauloaraujo.d5.framework.dominio.AgregadoExistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.dominio.spi.ISegurancaServico;
import dev.sauloaraujo.d5.framework.dominio.spi.NaoAutorizadoExcecao;
import io.cucumber.java.pt.Dado;
import io.cucumber.java.pt.Entao;
import io.cucumber.java.pt.Quando;
public final class MunicipioStepDefinitions {
private final Map<String, Municipio> municipios = new HashMap<>();
private final Set<String> unidadesFederativas = new HashSet<>();
private final Set<String> referenciados = new HashSet<>();
private final ISegurancaServico segurancaServico = mock(ISegurancaServico.class);
private final IMunicipioRepositorio repositorio = mock(IMunicipioRepositorio.class);
private final IUnidadeFederativaRepositorio unidadeFederativaRepositorio = mock(
IUnidadeFederativaRepositorio.class);
private final MunicipioServico servico = new MunicipioServico(segurancaServico, repositorio,
unidadeFederativaRepositorio);
private Municipio resultadoInclusao;
private Municipio resultadoBusca;
private RuntimeException excecaoCapturada;
public MunicipioStepDefinitions() {
when(unidadeFederativaRepositorio.existePorIdentificador(any()))
.thenAnswer(chamada -> unidadesFederativas.contains(chamada.getArgument(0).toString()));
when(repositorio.existePorIdentificador(any()))
.thenAnswer(chamada -> municipios.containsKey(chave(chamada.getArgument(0))));
when(repositorio.buscarPorIdentificador(any())).thenAnswer(chamada -> {
MunicipioCodigoIbge codigoIbge = chamada.getArgument(0);
var municipio = municipios.get(chave(codigoIbge));
if (municipio == null) {
throw new AgregadoInexistenteExcecao(
String.format(MunicipioServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge));
}
return municipio;
});
when(repositorio.existePorNome(any(), any(), any())).thenAnswer(chamada -> {
String nome = chamada.getArgument(0);
UnidadeFederativaCodigoIbge unidadeFederativa = chamada.getArgument(1);
Optional<MunicipioCodigoIbge> identificadorIgnorado = chamada.getArgument(2);
return municipios.values().stream()
.anyMatch(candidato -> candidato.getNome().equalsIgnoreCase(nome)
&& candidato.getUnidadeFederativa().equals(unidadeFederativa)
&& !foiIgnorado(candidato, identificadorIgnorado));
});
when(repositorio.existeReferencia(any()))
.thenAnswer(chamada -> referenciados.contains(chave(chamada.getArgument(0))));
when(repositorio.incluir(any())).thenAnswer(chamada -> {
Municipio municipio = chamada.getArgument(0);
municipios.put(chave(municipio.getIdentificador().orElseThrow()), municipio);
return municipio;
});
doAnswer(chamada -> {
Municipio municipio = chamada.getArgument(0);
municipios.put(chave(municipio.getIdentificador().orElseThrow()), municipio);
return null;
}).when(repositorio).alterar(any());
doAnswer(chamada -> {
MunicipioCodigoIbge codigoIbge = chamada.getArgument(0);
municipios.remove(chave(codigoIbge));
return null;
}).when(repositorio).excluir(any());
}
private static String chave(MunicipioCodigoIbge codigoIbge) {
return codigoIbge.toString();
}
private static boolean foiIgnorado(Municipio candidato, Optional<MunicipioCodigoIbge> identificadorIgnorado) {
return identificadorIgnorado.isPresent() && candidato.getIdentificador().equals(identificadorIgnorado);
}
private static Municipio criar(String codigoIbge, String nome) {
return new Municipio(new MunicipioCodigoIbge(codigoIbge), nome, Optional.empty());
}
@Dado("que não existe nenhum município cadastrado")
public void que_nao_existe_nenhum_municipio_cadastrado() {
municipios.clear();
unidadesFederativas.clear();
referenciados.clear();
}
@Dado("que existe a unidade federativa de código IBGE {string}")
public void que_existe_a_unidade_federativa(String codigoIbge) {
unidadesFederativas.add(codigoIbge);
}
@Dado("que sou um administrador do cadastro de municípios")
public void que_sou_um_administrador_do_cadastro_de_municipios() {
when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(true);
}
@Dado("que sou um usuário sem autorização para administrar municípios")
public void que_sou_um_usuario_sem_autorizacao_para_administrar_municipios() {
when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(false);
}
@Dado("que existe um município com código IBGE {string}, nome {string}")
public void que_existe_um_municipio(String codigoIbge, String nome) {
var municipio = criar(codigoIbge, nome);
municipios.put(chave(municipio.getIdentificador().orElseThrow()), municipio);
}
@Dado("que existe um município extinto com código IBGE {string}, nome {string}")
public void que_existe_um_municipio_extinto(String codigoIbge, String nome) {
var municipio = criar(codigoIbge, nome).extinguir();
municipios.put(chave(municipio.getIdentificador().orElseThrow()), municipio);
}
@Dado("que o município de código IBGE {string} é referenciado")
public void que_o_municipio_e_referenciado(String codigoIbge) {
referenciados.add(codigoIbge);
}
@Quando("eu incluo o município de código IBGE {string}, nome {string}")
public void eu_incluo_o_municipio(String codigoIbge, String nome) {
incluir(codigoIbge, nome);
}
@Quando("eu tento incluir o município de código IBGE {string}, nome {string}")
public void eu_tento_incluir_o_municipio(String codigoIbge, String nome) {
incluir(codigoIbge, nome);
}
@Quando("eu tento incluir o município extinto de código IBGE {string} e nome {string}")
public void eu_tento_incluir_o_municipio_extinto(String codigoIbge, String nome) {
incluir(criar(codigoIbge, nome).extinguir());
}
private void incluir(String codigoIbge, String nome) {
incluir(criar(codigoIbge, nome));
}
private void incluir(Municipio municipio) {
try {
resultadoInclusao = servico.incluir(municipio);
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu busco o município de código IBGE {string}")
public void eu_busco_o_municipio(String codigoIbge) {
buscar(codigoIbge);
}
@Quando("eu tento buscar o município de código IBGE {string}")
public void eu_tento_buscar_o_municipio(String codigoIbge) {
buscar(codigoIbge);
}
private void buscar(String codigoIbge) {
try {
resultadoBusca = servico.buscar(new MunicipioCodigoIbge(codigoIbge));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu altero o município de código IBGE {string} para nome {string}")
public void eu_altero_o_municipio(String codigoIbge, String nome) {
alterar(codigoIbge, nome);
}
@Quando("eu tento alterar o município de código IBGE {string} para nome {string}")
public void eu_tento_alterar_o_municipio(String codigoIbge, String nome) {
alterar(codigoIbge, nome);
}
@Quando("eu tento alterar o município de código IBGE {string} para extinto com nome {string}")
public void eu_tento_alterar_o_municipio_para_extinto(String codigoIbge, String nome) {
alterar(criar(codigoIbge, nome).extinguir());
}
private void alterar(String codigoIbge, String nome) {
alterar(criar(codigoIbge, nome));
}
private void alterar(Municipio municipio) {
try {
servico.alterar(municipio);
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu excluo o município de código IBGE {string}")
public void eu_excluo_o_municipio(String codigoIbge) {
excluir(codigoIbge);
}
@Quando("eu tento excluir o município de código IBGE {string}")
public void eu_tento_excluir_o_municipio(String codigoIbge) {
excluir(codigoIbge);
}
private void excluir(String codigoIbge) {
try {
servico.excluir(new MunicipioCodigoIbge(codigoIbge));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Quando("eu extingo o município de código IBGE {string}")
public void eu_extingo_o_municipio(String codigoIbge) {
extinguir(codigoIbge);
}
@Quando("eu tento extinguir o município de código IBGE {string}")
public void eu_tento_extinguir_o_municipio(String codigoIbge) {
extinguir(codigoIbge);
}
private void extinguir(String codigoIbge) {
try {
servico.extinguir(new MunicipioCodigoIbge(codigoIbge));
} catch (RuntimeException excecao) {
excecaoCapturada = excecao;
}
}
@Entao("o município é incluído com sucesso")
public void o_municipio_e_incluido_com_sucesso() {
assertNull(excecaoCapturada);
assertNotNull(resultadoInclusao);
}
@Entao("o município de código IBGE {string} passa a existir")
public void o_municipio_passa_a_existir(String codigoIbge) {
assertTrue(municipios.containsKey(codigoIbge));
}
@Entao("o município de código IBGE {string} continua a existir")
public void o_municipio_continua_a_existir(String codigoIbge) {
assertTrue(municipios.containsKey(codigoIbge));
}
@Entao("a inclusão do município é rejeitada por falta de autorização")
public void a_inclusao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a inclusão é rejeitada porque já existe um município com o identificador {string}")
public void a_inclusao_e_rejeitada_pelo_identificador(String codigoIbge) {
assertMensagemDeExistencia(MunicipioServico.IDENTIFICADOR_EXISTENTE_MODELO, codigoIbge);
}
@Entao("a inclusão é rejeitada porque já existe um município com o nome {string} na unidade federativa")
public void a_inclusao_e_rejeitada_pelo_nome(String nome) {
assertMensagemDeExistencia(MunicipioServico.NOME_EXISTENTE_MODELO, nome);
}
@Entao("a inclusão é rejeitada porque não existe unidade federativa com o identificador {string}")
public void a_inclusao_e_rejeitada_por_unidade_federativa_inexistente(String codigoIbge) {
assertInstanceOf(AgregadoInexistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge),
excecaoCapturada.getMessage());
}
@Entao("o município encontrado tem nome {string}")
public void o_municipio_encontrado_tem(String nome) {
assertEquals(nome, resultadoBusca.getNome());
}
@Entao("a busca é rejeitada porque não existe município com o identificador {string}")
public void a_busca_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
@Entao("o município é alterado com sucesso")
public void o_municipio_e_alterado_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("o município de código IBGE {string} passa a ter nome {string}")
public void o_municipio_passa_a_ter(String codigoIbge, String nome) {
var municipio = municipios.get(codigoIbge);
assertNotNull(municipio);
assertEquals(nome, municipio.getNome());
}
@Entao("a alteração do município é rejeitada por falta de autorização")
public void a_alteracao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a alteração é rejeitada porque não existe município com o identificador {string}")
public void a_alteracao_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
@Entao("a inclusão é rejeitada porque o município só pode ser extinto pela ação de extinção")
public void a_inclusao_e_rejeitada_por_extincao_indevida() {
assertMensagemDeExtincaoIndevida();
}
@Entao("a alteração é rejeitada porque o município só pode ser extinto pela ação de extinção")
public void a_alteracao_e_rejeitada_por_extincao_indevida() {
assertMensagemDeExtincaoIndevida();
}
@Entao("a alteração é rejeitada porque o município {string} está extinto")
public void a_alteracao_e_rejeitada_por_extincao(String codigoIbge) {
assertMensagemDeExtincao(codigoIbge);
}
@Entao("o município é excluído com sucesso")
public void o_municipio_e_excluido_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("o município de código IBGE {string} deixa de existir")
public void o_municipio_deixa_de_existir(String codigoIbge) {
assertFalse(municipios.containsKey(codigoIbge));
}
@Entao("a exclusão do município é rejeitada por falta de autorização")
public void a_exclusao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a exclusão é rejeitada porque não existe município com o identificador {string}")
public void a_exclusao_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
@Entao("a exclusão é rejeitada porque o município {string} é referenciado")
public void a_exclusao_e_rejeitada_por_referencia(String codigoIbge) {
assertInstanceOf(IllegalStateException.class, excecaoCapturada);
assertEquals(String.format(MunicipioServico.REFERENCIADO_MODELO, codigoIbge), excecaoCapturada.getMessage());
}
@Entao("o município é extinguido com sucesso")
public void o_municipio_e_extinguido_com_sucesso() {
assertNull(excecaoCapturada);
}
@Entao("o município de código IBGE {string} passa a estar extinto")
public void o_municipio_passa_a_estar_extinto(String codigoIbge) {
var municipio = municipios.get(codigoIbge);
assertNotNull(municipio);
assertTrue(municipio.isExtinto());
}
@Entao("a extinção do município é rejeitada por falta de autorização")
public void a_extincao_e_rejeitada_por_falta_de_autorizacao() {
assertInstanceOf(NaoAutorizadoExcecao.class, excecaoCapturada);
}
@Entao("a extinção é rejeitada porque não existe município com o identificador {string}")
public void a_extincao_e_rejeitada_por_inexistencia(String codigoIbge) {
assertMensagemDeInexistencia(codigoIbge);
}
@Entao("a extinção é rejeitada porque o município {string} está extinto")
public void a_extincao_e_rejeitada_por_extincao(String codigoIbge) {
assertMensagemDeExtincao(codigoIbge);
}
private void assertMensagemDeExistencia(String modelo, Object argumento) {
assertInstanceOf(AgregadoExistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(modelo, argumento), excecaoCapturada.getMessage());
}
private void assertMensagemDeInexistencia(Object argumento) {
assertInstanceOf(AgregadoInexistenteExcecao.class, excecaoCapturada);
assertEquals(String.format(MunicipioServico.IDENTIFICADOR_INEXISTENTE_MODELO, argumento),
excecaoCapturada.getMessage());
}
private void assertMensagemDeExtincaoIndevida() {
assertInstanceOf(IllegalArgumentException.class, excecaoCapturada);
assertEquals(MunicipioServico.EXTINCAO_INDEVIDA, excecaoCapturada.getMessage());
}
private void assertMensagemDeExtincao(Object argumento) {
assertInstanceOf(IllegalStateException.class, excecaoCapturada);
assertEquals(String.format(Municipio.EXTINTO_MODELO, argumento), excecaoCapturada.getMessage());
}
}
Execute os cenários dos dois subdomínios — com -am, o Maven inclui no build dominio-geografia, do qual dominio-crm agora depende, e executa também os seus cenários:
mvn -pl dominio-crm -am test
O resultado deve informar 43 testes em Geografia e 19 no CRM, sem falhas.
Aplicação
As classes de aplicação do cliente ficam no pacote com.example.exemplo.aplicacao.crm.cliente. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.crm.cliente;
Projeção
A projeção traz, além do identificador e do nome do cliente, o código e o nome do município e o código e o nome da unidade federativa do endereço. O construtor usado pela consulta JPQL recebe o identificador como Long, o tipo usado pela entidade JPA.
package com.example.exemplo.aplicacao.crm.cliente;
import java.util.Objects;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.ClienteIdentificador;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import dev.sauloaraujo.d5.framework.aplicacao.IProjecao;
public final class ClienteProjecao implements IProjecao<Cliente> {
private final ClienteIdentificador identificador;
private final String nome;
private final MunicipioCodigoIbge municipio;
private final String municipioNome;
private final UnidadeFederativaCodigoIbge unidadeFederativa;
private final String unidadeFederativaNome;
public ClienteProjecao(ClienteIdentificador identificador, String nome, MunicipioCodigoIbge municipio,
String municipioNome, UnidadeFederativaCodigoIbge unidadeFederativa, String unidadeFederativaNome) {
this.identificador = Objects.requireNonNull(identificador);
this.nome = Objects.requireNonNull(nome);
this.municipio = Objects.requireNonNull(municipio);
this.municipioNome = Objects.requireNonNull(municipioNome);
this.unidadeFederativa = Objects.requireNonNull(unidadeFederativa);
this.unidadeFederativaNome = Objects.requireNonNull(unidadeFederativaNome);
}
public ClienteProjecao(Long identificador, String nome, String municipio, String municipioNome,
String unidadeFederativa, String unidadeFederativaNome) {
this(new ClienteIdentificador(identificador.toString()), nome, new MunicipioCodigoIbge(municipio),
municipioNome, new UnidadeFederativaCodigoIbge(unidadeFederativa), unidadeFederativaNome);
}
public ClienteIdentificador getIdentificador() {
return identificador;
}
public String getNome() {
return nome;
}
public MunicipioCodigoIbge getMunicipio() {
return municipio;
}
public String getMunicipioNome() {
return municipioNome;
}
public UnidadeFederativaCodigoIbge getUnidadeFederativa() {
return unidadeFederativa;
}
public String getUnidadeFederativaNome() {
return unidadeFederativaNome;
}
}
Ordenação
Os clientes podem ser ordenados pelo identificador, pelo nome, pelo nome do município e pelo nome da unidade federativa.
package com.example.exemplo.aplicacao.crm.cliente;
import dev.sauloaraujo.d5.framework.aplicacao.IOrdenacao;
public enum ClienteOrdenacao implements IOrdenacao<ClienteProjecao> {
IDENTIFICADOR, NOME, MUNICIPIO_NOME, UNIDADE_FEDERATIVA_NOME
}
Consulta
A consulta filtra os clientes por um trecho do nome, pelo código do município e pelo código da unidade federativa.
package com.example.exemplo.aplicacao.crm.cliente;
import java.util.Objects;
import java.util.Optional;
import dev.sauloaraujo.d5.framework.aplicacao.impl.Consulta;
public final class ClienteConsulta extends Consulta<ClienteOrdenacao> {
private final Optional<String> nome;
private final Optional<String> municipio;
private final Optional<String> unidadeFederativa;
public ClienteConsulta(Optional<ClienteOrdenacao> ordenacao, Optional<Boolean> descendente,
Optional<Integer> indicePagina, Optional<Integer> tamanhoPagina, Optional<String> nome,
Optional<String> municipio, Optional<String> unidadeFederativa) {
super(ordenacao, descendente, indicePagina, tamanhoPagina);
this.nome = Objects.requireNonNull(nome);
this.municipio = Objects.requireNonNull(municipio);
this.unidadeFederativa = Objects.requireNonNull(unidadeFederativa);
}
public Optional<String> getNome() {
return nome;
}
public Optional<String> getMunicipio() {
return municipio;
}
public Optional<String> getUnidadeFederativa() {
return unidadeFederativa;
}
}
Repositório de aplicação e serviço de aplicação
Crie a interface do repositório de aplicação no pacote spi e o serviço de aplicação, como nas lições anteriores:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.crm.cliente.spi;
package com.example.exemplo.aplicacao.crm.cliente.spi;
import com.example.exemplo.aplicacao.crm.cliente.ClienteConsulta;
import com.example.exemplo.aplicacao.crm.cliente.ClienteProjecao;
import dev.sauloaraujo.d5.framework.aplicacao.spi.IAgregadoAplicacaoRepositorio;
public interface IClienteAplicacaoRepositorio extends IAgregadoAplicacaoRepositorio<ClienteProjecao, ClienteConsulta> {
}
package com.example.exemplo.aplicacao.crm.cliente;
import com.example.exemplo.aplicacao.crm.cliente.spi.IClienteAplicacaoRepositorio;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.ClienteIdentificador;
import com.example.exemplo.dominio.crm.cliente.ClienteServico;
import dev.sauloaraujo.d5.framework.aplicacao.impl.AgregadoAplicacaoServicoAbstrato;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
public final class ClienteAplicacaoServico extends
AgregadoAplicacaoServicoAbstrato<ClienteIdentificador, Cliente, AgregadoAutorizacoes<Cliente>, ClienteProjecao, ClienteConsulta> {
public ClienteAplicacaoServico(IClienteAplicacaoRepositorio repositorio, ClienteServico servico) {
super(repositorio, servico);
}
}
Infraestrutura
A persistência de cliente fica no pacote com.example.exemplo.infraestrutura.crm.cliente. Crie o package-info.java desse pacote, anotado com @NullMarked:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.infraestrutura.crm.cliente;
Entidade JPA
O identificador é um Long gerado pelo banco de dados — por isso é anotado com @GeneratedValue e admite null antes da inclusão. Os atributos do endereço são mapeados para colunas da própria tabela de clientes. Já o município, como a unidade federativa na entidade JPA do município, é mapeado por uma associação @ManyToOne preguiçosa com MunicipioJpa, cuja coluna, MUN_ID, é uma chave estrangeira — enquanto o endereço continua referenciando o município pelo código IBGE. Diferente da do município, porém, essa coluna pode ser alterada, já que o cliente pode mudar de município. Além da do identificador, a coluna do complemento, opcional, é a única sem @NotNull. A coluna do CPF é única, garantindo no banco de dados a mesma unicidade validada pelo serviço.
package com.example.exemplo.infraestrutura.crm.cliente;
import org.jspecify.annotations.Nullable;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.Cpf;
import com.example.exemplo.dominio.crm.cliente.EnderecoCep;
import com.example.exemplo.infraestrutura.geografia.municipio.MunicipioJpa;
import dev.sauloaraujo.d5.framework.infraestrutura.IAgregadoJpa;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "CLI_CLIENTE")
public class ClienteJpa implements IAgregadoJpa<Cliente> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "CLI_ID", updatable = false)
private @Nullable Long identificador;
@Column(name = "CLI_CPF", unique = true)
@NotNull
@Size(max = Cpf.TAMANHO)
private String cpf;
@Column(name = "CLI_NM")
@NotNull
@Size(max = 100)
private String nome;
@Column(name = "CLI_LOGRADOURO")
@NotNull
@Size(max = 100)
private String logradouro;
@Column(name = "CLI_NUMERO")
@NotNull
@Size(max = 10)
private String numero;
@Column(name = "CLI_COMPLEMENTO")
@Size(max = 50)
private @Nullable String complemento;
@Column(name = "CLI_BAIRRO")
@NotNull
@Size(max = 50)
private String bairro;
@Column(name = "CLI_CEP")
@NotNull
@Size(max = EnderecoCep.TAMANHO)
private String cep;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MUN_ID")
@NotNull
private MunicipioJpa municipio;
protected ClienteJpa() {
}
public ClienteJpa(@Nullable Long identificador, String cpf, String nome, String logradouro, String numero,
@Nullable String complemento, String bairro, String cep, MunicipioJpa municipio) {
this.identificador = identificador;
this.cpf = cpf;
this.nome = nome;
this.logradouro = logradouro;
this.numero = numero;
this.complemento = complemento;
this.bairro = bairro;
this.cep = cep;
this.municipio = municipio;
}
public @Nullable Long getIdentificador() {
return identificador;
}
public String getCpf() {
return cpf;
}
public String getNome() {
return nome;
}
public String getLogradouro() {
return logradouro;
}
public String getNumero() {
return numero;
}
public @Nullable String getComplemento() {
return complemento;
}
public String getBairro() {
return bairro;
}
public String getCep() {
return cep;
}
public MunicipioJpa getMunicipio() {
return municipio;
}
}
Repositório Spring Data
existePorCpf segue o padrão das verificações de unicidade das lições anteriores. A consulta de pesquisar faz duas junções pelas associações: com o município do endereço — join c.municipio m — e, a partir dele, com a unidade federativa — join m.unidadeFederativa u. O filtro pelo município compara m.identificador, e existeReferencia, no repositório de município, c.municipio.identificador.
package com.example.exemplo.infraestrutura.crm.cliente;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.example.exemplo.aplicacao.crm.cliente.ClienteProjecao;
public interface IClienteRepositorioJpa extends JpaRepository<ClienteJpa, Long> {
@Query("""
select count(c) > 0
from ClienteJpa c
where c.cpf = :cpf
and (:identificadorIgnorado is null or c.identificador <> :identificadorIgnorado)
""")
boolean existePorCpf(String cpf, @Nullable Long identificadorIgnorado);
@Query("""
select new com.example.exemplo.aplicacao.crm.cliente.ClienteProjecao(c.identificador, c.nome, m.identificador, m.nome, u.identificador, u.nome)
from ClienteJpa c
join c.municipio m
join m.unidadeFederativa u
where (:nome is null or lower(c.nome) like :nome escape '\\')
and (:municipio is null or m.identificador = :municipio)
and (:unidadeFederativa is null or u.identificador = :unidadeFederativa)
""")
Page<ClienteProjecao> pesquisar(@Nullable String nome, @Nullable String municipio,
@Nullable String unidadeFederativa, Pageable pageable);
}
Implementação do repositório
paraIdentificadorJpa converte o identificador do domínio para o Long da entidade JPA, e paraAgregado faz a conversão inversa — exigindo, com Objects.requireNonNull, que a entidade lida do banco de dados tenha identificador. Como o domínio trata o identificador apenas como texto, ele pode receber um valor que não é um Long — abc, por exemplo. Nenhum cliente pode ter esse identificador, por isso paraIdentificadorJpa lança, nesse caso, AgregadoInexistenteExcecao com a mesma mensagem de um identificador inexistente, e existePorIdentificador é sobrescrito para devolver false. Assim, buscar, alterar ou excluir o cliente abc responde 404, como para qualquer outro cliente inexistente, sem expor o tipo usado pela infraestrutura. paraJpa e paraAgregado também convertem o endereço de e para as colunas da entidade JPA. Como no repositório de município, paraJpa preenche a associação com getReferenceById, aqui de IMunicipioRepositorioJpa, recebido no construtor. Já paraAgregado, diferente do repositório de município, precisa ler a associação: lê dela apenas o código do município, que a referência preguiçosa fornece sem carregá-lo — inclusive fora de uma transação, como nas requisições GET, já que o open session in view está desativado.
package com.example.exemplo.infraestrutura.crm.cliente;
import java.util.Objects;
import java.util.Optional;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.ClienteIdentificador;
import com.example.exemplo.dominio.crm.cliente.ClienteServico;
import com.example.exemplo.dominio.crm.cliente.Cpf;
import com.example.exemplo.dominio.crm.cliente.Endereco;
import com.example.exemplo.dominio.crm.cliente.EnderecoCep;
import com.example.exemplo.dominio.crm.cliente.spi.IClienteRepositorio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.infraestrutura.geografia.municipio.IMunicipioRepositorioJpa;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;
public class ClienteRepositorio extends AgregadoRepositorioAbstrato<ClienteIdentificador, Cliente, Long, ClienteJpa>
implements IClienteRepositorio {
private final IClienteRepositorioJpa repositorioJpa;
private final IMunicipioRepositorioJpa municipioRepositorioJpa;
public ClienteRepositorio(IClienteRepositorioJpa repositorioJpa, IMunicipioRepositorioJpa municipioRepositorioJpa) {
super(repositorioJpa, ClienteServico.IDENTIFICADOR_INEXISTENTE_MODELO);
this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
this.municipioRepositorioJpa = Objects.requireNonNull(municipioRepositorioJpa);
}
@Override
protected ClienteJpa paraJpa(Cliente agregado) {
Objects.requireNonNull(agregado);
var identificador = agregado.getIdentificador().map(this::paraIdentificadorJpa).orElse(null);
var endereco = agregado.getEndereco();
var municipio = municipioRepositorioJpa.getReferenceById(endereco.getMunicipio().toString());
return new ClienteJpa(identificador, agregado.getCpf().toString(), agregado.getNome(), endereco.getLogradouro(),
endereco.getNumero(), endereco.getComplemento().orElse(null), endereco.getBairro(),
endereco.getCep().toString(), municipio);
}
@Override
protected Cliente paraAgregado(ClienteJpa agregadoJpa) {
Objects.requireNonNull(agregadoJpa);
var identificador = new ClienteIdentificador(Objects.requireNonNull(agregadoJpa.getIdentificador()).toString());
var endereco = new Endereco(agregadoJpa.getLogradouro(), agregadoJpa.getNumero(),
Optional.ofNullable(agregadoJpa.getComplemento()), agregadoJpa.getBairro(),
new EnderecoCep(agregadoJpa.getCep()),
new MunicipioCodigoIbge(agregadoJpa.getMunicipio().getIdentificador()));
return new Cliente(Optional.of(identificador), new Cpf(agregadoJpa.getCpf()), agregadoJpa.getNome(), endereco);
}
@Override
protected Long paraIdentificadorJpa(ClienteIdentificador identificador) {
Objects.requireNonNull(identificador);
return converter(identificador).orElseThrow(() -> {
var mensagem = ClienteServico.IDENTIFICADOR_INEXISTENTE_MODELO.formatted(identificador);
return new AgregadoInexistenteExcecao(mensagem);
});
}
private static Optional<Long> converter(ClienteIdentificador identificador) {
try {
return Optional.of(Long.valueOf(identificador.toString()));
} catch (NumberFormatException excecao) {
return Optional.empty();
}
}
@Override
public boolean existePorIdentificador(ClienteIdentificador identificador) {
Objects.requireNonNull(identificador);
return converter(identificador).isPresent() && super.existePorIdentificador(identificador);
}
@Override
public boolean existePorCpf(Cpf cpf, Optional<ClienteIdentificador> identificadorIgnorado) {
Objects.requireNonNull(cpf);
Objects.requireNonNull(identificadorIgnorado);
var identificadorIgnoradoJpa = identificadorIgnorado.map(this::paraIdentificadorJpa).orElse(null);
return repositorioJpa.existePorCpf(cpf.toString(), identificadorIgnoradoJpa);
}
}
Implementação do repositório de aplicação
As ordenações pelo nome do município e pelo nome da unidade federativa são mapeadas para m.nome e u.nome, os aliases das junções declaradas na consulta de pesquisar.
package com.example.exemplo.infraestrutura.crm.cliente;
import java.util.Objects;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.example.exemplo.aplicacao.crm.cliente.ClienteConsulta;
import com.example.exemplo.aplicacao.crm.cliente.ClienteOrdenacao;
import com.example.exemplo.aplicacao.crm.cliente.ClienteProjecao;
import com.example.exemplo.aplicacao.crm.cliente.spi.IClienteAplicacaoRepositorio;
import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoAplicacaoRepositorioAbstrato;
public class ClienteAplicacaoRepositorio
extends AgregadoAplicacaoRepositorioAbstrato<ClienteProjecao, ClienteOrdenacao, ClienteConsulta>
implements IClienteAplicacaoRepositorio {
private final IClienteRepositorioJpa repositorioJpa;
public ClienteAplicacaoRepositorio(IClienteRepositorioJpa repositorioJpa) {
this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
}
@Override
protected Page<ClienteProjecao> pesquisar(ClienteConsulta consulta, Pageable pageable) {
Objects.requireNonNull(consulta);
Objects.requireNonNull(pageable);
var nome = paraLike(consulta.getNome());
var municipio = consulta.getMunicipio().orElse(null);
var unidadeFederativa = consulta.getUnidadeFederativa().orElse(null);
return repositorioJpa.pesquisar(nome, municipio, unidadeFederativa, pageable);
}
@Override
protected String paraPropriedadeOrdenacao(ClienteOrdenacao ordenacao) {
Objects.requireNonNull(ordenacao);
return switch (ordenacao) {
case IDENTIFICADOR -> "identificador";
case NOME -> "nome";
case MUNICIPIO_NOME -> "m.nome";
case UNIDADE_FEDERATIVA_NOME -> "u.nome";
};
}
}
Município referenciado
Implemente existeReferencia no repositório de município, com uma consulta que verifica se algum cliente referencia o município:
package com.example.exemplo.infraestrutura.geografia.municipio;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioProjecao;
public interface IMunicipioRepositorioJpa extends JpaRepository<MunicipioJpa, String> {
@Query("""
select count(m) > 0
from MunicipioJpa m
where lower(m.nome) = lower(:nome)
and m.unidadeFederativa.identificador = :unidadeFederativa
and (:identificadorIgnorado is null or m.identificador <> :identificadorIgnorado)
""")
boolean existePorNome(String nome, String unidadeFederativa, @Nullable String identificadorIgnorado);
@Query("""
select count(c) > 0
from ClienteJpa c
where c.municipio.identificador = :identificador
""")
boolean existeReferencia(String identificador);
@Query("""
select new com.example.exemplo.aplicacao.geografia.municipio.MunicipioProjecao(m.identificador, m.nome, u.identificador, u.nome, m.extinto)
from MunicipioJpa m
join m.unidadeFederativa u
where (:nome is null or lower(m.nome) like :nome escape '\\')
and (:unidadeFederativa is null or u.identificador = :unidadeFederativa)
""")
Page<MunicipioProjecao> pesquisar(@Nullable String nome, @Nullable String unidadeFederativa, Pageable pageable);
}
package com.example.exemplo.infraestrutura.geografia.municipio;
import java.util.Objects;
import java.util.Optional;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.municipio.MunicipioServico;
import com.example.exemplo.dominio.geografia.municipio.spi.IMunicipioRepositorio;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.IUnidadeFederativaRepositorioJpa;
import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;
public class MunicipioRepositorio
extends AgregadoRepositorioAbstrato<MunicipioCodigoIbge, Municipio, String, MunicipioJpa>
implements IMunicipioRepositorio {
private final IMunicipioRepositorioJpa repositorioJpa;
private final IUnidadeFederativaRepositorioJpa unidadeFederativaRepositorioJpa;
public MunicipioRepositorio(IMunicipioRepositorioJpa repositorioJpa,
IUnidadeFederativaRepositorioJpa unidadeFederativaRepositorioJpa) {
super(repositorioJpa, MunicipioServico.IDENTIFICADOR_INEXISTENTE_MODELO);
this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
this.unidadeFederativaRepositorioJpa = Objects.requireNonNull(unidadeFederativaRepositorioJpa);
}
@Override
protected MunicipioJpa paraJpa(Municipio agregado) {
Objects.requireNonNull(agregado);
var identificador = agregado.getIdentificador().orElseThrow().toString();
var unidadeFederativa = unidadeFederativaRepositorioJpa
.getReferenceById(agregado.getUnidadeFederativa().toString());
return new MunicipioJpa(identificador, agregado.getNome(), unidadeFederativa, agregado.isExtinto());
}
@Override
protected Municipio paraAgregado(MunicipioJpa agregadoJpa) {
Objects.requireNonNull(agregadoJpa);
var identificador = new MunicipioCodigoIbge(agregadoJpa.getIdentificador());
return new Municipio(identificador, agregadoJpa.getNome(), Optional.of(agregadoJpa.isExtinto()));
}
@Override
protected String paraIdentificadorJpa(MunicipioCodigoIbge identificador) {
Objects.requireNonNull(identificador);
return identificador.toString();
}
@Override
public boolean existePorNome(String nome, UnidadeFederativaCodigoIbge unidadeFederativa,
Optional<MunicipioCodigoIbge> identificadorIgnorado) {
Objects.requireNonNull(nome);
Objects.requireNonNull(unidadeFederativa);
Objects.requireNonNull(identificadorIgnorado);
var identificadorIgnoradoJpa = identificadorIgnorado.map(MunicipioCodigoIbge::toString).orElse(null);
return repositorioJpa.existePorNome(nome, unidadeFederativa.toString(), identificadorIgnoradoJpa);
}
@Override
public boolean existeReferencia(MunicipioCodigoIbge identificador) {
Objects.requireNonNull(identificador);
return repositorioJpa.existeReferencia(identificador.toString());
}
}
É no módulo de infraestrutura, que conhece as entidades JPA de todos os subdomínios, que a referência do CRM ao subdomínio Geografia é verificada.
Testes do repositório
Escreva os testes do repositório de cliente, cobrindo as operações do repositório — inclusive a geração do identificador na inclusão e um cliente sem complemento — e as variações de existePorCpf. Por causa das chaves estrangeiras, os testes persistem a unidade federativa e o município antes dos clientes:
package com.example.exemplo.infraestrutura.crm.cliente;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.ClienteIdentificador;
import com.example.exemplo.dominio.crm.cliente.Cpf;
import com.example.exemplo.dominio.crm.cliente.Endereco;
import com.example.exemplo.dominio.crm.cliente.EnderecoCep;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.infraestrutura.geografia.municipio.IMunicipioRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.municipio.MunicipioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.IUnidadeFederativaRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.UnidadeFederativaJpa;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
@DataJpaTest
class ClienteRepositorioTest {
private static final MunicipioCodigoIbge SAO_PAULO = new MunicipioCodigoIbge("3550308");
private static final Cpf CPF_MARIA = new Cpf("11111111111");
private static final Cpf CPF_ANA = new Cpf("22222222222");
@Autowired
private IClienteRepositorioJpa repositorioJpa;
@Autowired
private IMunicipioRepositorioJpa municipioRepositorioJpa;
@Autowired
private IUnidadeFederativaRepositorioJpa unidadeFederativaRepositorioJpa;
private ClienteRepositorio repositorio;
@BeforeEach
void configurar() {
repositorio = new ClienteRepositorio(repositorioJpa, municipioRepositorioJpa);
var saoPaulo = unidadeFederativaRepositorioJpa.save(new UnidadeFederativaJpa("35", "SP", "São Paulo"));
municipioRepositorioJpa.save(new MunicipioJpa("3550308", "São Paulo", saoPaulo, false));
}
private static Endereco criarEndereco(Optional<String> complemento) {
return new Endereco("Avenida Paulista", "1000", complemento, "Bela Vista", new EnderecoCep("01310100"),
SAO_PAULO);
}
private static Cliente criar(Cpf cpf, String nome) {
return criar(cpf, nome, Optional.empty());
}
private static Cliente criar(Cpf cpf, String nome, Optional<String> complemento) {
return new Cliente(Optional.empty(), cpf, nome, criarEndereco(complemento));
}
@Test
void incluiGerandoIdentificador() {
var cliente = repositorio.incluir(criar(CPF_MARIA, "Maria", Optional.of("Sala 12")));
var identificador = cliente.getIdentificador().orElseThrow();
var buscado = repositorio.buscarPorIdentificador(identificador);
assertEquals(CPF_MARIA, buscado.getCpf());
assertEquals("Maria", buscado.getNome());
assertEquals("Sala 12", buscado.getEndereco().getComplemento().orElseThrow());
assertEquals(SAO_PAULO, buscado.getEndereco().getMunicipio());
assertEquals(new EnderecoCep("01310100"), buscado.getEndereco().getCep());
}
@Test
void incluiSemComplemento() {
var cliente = repositorio.incluir(criar(CPF_MARIA, "Maria"));
var buscado = repositorio.buscarPorIdentificador(cliente.getIdentificador().orElseThrow());
assertTrue(buscado.getEndereco().getComplemento().isEmpty());
}
@Test
void buscaPorIdentificadorInexistenteLancaExcecao() {
assertThrows(AgregadoInexistenteExcecao.class,
() -> repositorio.buscarPorIdentificador(new ClienteIdentificador("999")));
}
@Test
void buscaPorIdentificadorNaoNumericoLancaExcecao() {
var excecao = assertThrows(AgregadoInexistenteExcecao.class,
() -> repositorio.buscarPorIdentificador(new ClienteIdentificador("abc")));
assertEquals("Não existe cliente com o identificador abc", excecao.getMessage());
}
@Test
void existePorIdentificador() {
var cliente = repositorio.incluir(criar(CPF_MARIA, "Maria"));
assertTrue(repositorio.existePorIdentificador(cliente.getIdentificador().orElseThrow()));
assertFalse(repositorio.existePorIdentificador(new ClienteIdentificador("999")));
}
@Test
void naoExistePorIdentificadorNaoNumerico() {
assertFalse(repositorio.existePorIdentificador(new ClienteIdentificador("abc")));
assertFalse(repositorio.existePorIdentificador(new ClienteIdentificador("99999999999999999999")));
}
@Test
void altera() {
var identificador = repositorio.incluir(criar(CPF_MARIA, "Maria")).getIdentificador();
repositorio.alterar(new Cliente(identificador, CPF_MARIA, "Maria Silva", criarEndereco(Optional.empty())));
assertEquals("Maria Silva", repositorio.buscarPorIdentificador(identificador.orElseThrow()).getNome());
}
@Test
void exclui() {
var identificador = repositorio.incluir(criar(CPF_MARIA, "Maria")).getIdentificador().orElseThrow();
repositorio.excluir(identificador);
assertFalse(repositorio.existePorIdentificador(identificador));
}
@Test
void existePorCpfConsiderandoTodosOsClientes() {
repositorio.incluir(criar(CPF_MARIA, "Maria"));
assertTrue(repositorio.existePorCpf(CPF_MARIA, Optional.empty()));
assertFalse(repositorio.existePorCpf(CPF_ANA, Optional.empty()));
}
@Test
void existePorCpfIgnorandoOProprioIdentificador() {
var identificador = repositorio.incluir(criar(CPF_MARIA, "Maria")).getIdentificador();
assertFalse(repositorio.existePorCpf(CPF_MARIA, identificador));
}
@Test
void existePorCpfNaoIgnorandoUmClienteDiferente() {
repositorio.incluir(criar(CPF_MARIA, "Maria"));
var identificador = repositorio.incluir(criar(CPF_ANA, "Ana")).getIdentificador();
assertTrue(repositorio.existePorCpf(CPF_MARIA, identificador));
}
}
Como as junções da pesquisa são internas, os testes do repositório de aplicação persistem as unidades federativas e os municípios antes dos clientes. Além dos filtros, eles cobrem as ordenações pelo nome do município e da unidade federativa.
package com.example.exemplo.infraestrutura.crm.cliente;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import com.example.exemplo.aplicacao.crm.cliente.ClienteConsulta;
import com.example.exemplo.aplicacao.crm.cliente.ClienteOrdenacao;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.Cpf;
import com.example.exemplo.dominio.crm.cliente.Endereco;
import com.example.exemplo.dominio.crm.cliente.EnderecoCep;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.infraestrutura.geografia.municipio.IMunicipioRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.municipio.MunicipioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.IUnidadeFederativaRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.UnidadeFederativaJpa;
@DataJpaTest
class ClienteAplicacaoRepositorioTest {
@Autowired
private IClienteRepositorioJpa repositorioJpa;
@Autowired
private IMunicipioRepositorioJpa municipioRepositorioJpa;
@Autowired
private IUnidadeFederativaRepositorioJpa unidadeFederativaRepositorioJpa;
private ClienteRepositorio repositorio;
private ClienteAplicacaoRepositorio repositorioAplicacao;
@BeforeEach
void configurar() {
repositorio = new ClienteRepositorio(repositorioJpa, municipioRepositorioJpa);
repositorioAplicacao = new ClienteAplicacaoRepositorio(repositorioJpa);
unidadeFederativaRepositorioJpa.save(new UnidadeFederativaJpa("35", "SP", "São Paulo"));
unidadeFederativaRepositorioJpa.save(new UnidadeFederativaJpa("33", "RJ", "Rio de Janeiro"));
municipioRepositorioJpa.save(new MunicipioJpa("3550308", "São Paulo",
unidadeFederativaRepositorioJpa.getReferenceById("35"), false));
municipioRepositorioJpa.save(
new MunicipioJpa("3548500", "Santos", unidadeFederativaRepositorioJpa.getReferenceById("35"), false));
municipioRepositorioJpa.save(new MunicipioJpa("3304557", "Rio de Janeiro",
unidadeFederativaRepositorioJpa.getReferenceById("33"), false));
}
private static Cliente criar(String cpf, String nome, String municipio) {
var endereco = new Endereco("Rua A", "10", Optional.empty(), "Centro", new EnderecoCep("01310100"),
new MunicipioCodigoIbge(municipio));
return new Cliente(Optional.empty(), new Cpf(cpf), nome, endereco);
}
private static ClienteConsulta consultar(Optional<String> nome, Optional<String> municipio,
Optional<String> unidadeFederativa) {
return consultar(ClienteOrdenacao.NOME, nome, municipio, unidadeFederativa);
}
private static ClienteConsulta consultar(ClienteOrdenacao ordenacao, Optional<String> nome,
Optional<String> municipio, Optional<String> unidadeFederativa) {
return new ClienteConsulta(Optional.of(ordenacao), Optional.empty(), Optional.empty(), Optional.empty(), nome,
municipio, unidadeFederativa);
}
@Test
void pesquisaRetornaProjecaoOrdenadaPorNome() {
repositorio.incluir(criar("11111111111", "Maria", "3550308"));
repositorio.incluir(criar("22222222222", "Ana", "3304557"));
var resultado = repositorioAplicacao.pesquisar(consultar(Optional.empty(), Optional.empty(), Optional.empty()));
assertEquals(2, resultado.getTotal());
var primeira = resultado.getProjecoes().get(0);
assertEquals("Ana", primeira.getNome());
assertEquals(new MunicipioCodigoIbge("3304557"), primeira.getMunicipio());
assertEquals("Rio de Janeiro", primeira.getMunicipioNome());
assertEquals(new UnidadeFederativaCodigoIbge("33"), primeira.getUnidadeFederativa());
assertEquals("Rio de Janeiro", primeira.getUnidadeFederativaNome());
assertEquals("Maria", resultado.getProjecoes().get(1).getNome());
}
@Test
void pesquisaOrdenadaPorNomeDoMunicipio() {
repositorio.incluir(criar("11111111111", "Ana", "3550308"));
repositorio.incluir(criar("22222222222", "Maria", "3548500"));
var resultado = repositorioAplicacao.pesquisar(
consultar(ClienteOrdenacao.MUNICIPIO_NOME, Optional.empty(), Optional.empty(), Optional.empty()));
assertEquals("Maria", resultado.getProjecoes().get(0).getNome());
assertEquals("Ana", resultado.getProjecoes().get(1).getNome());
}
@Test
void pesquisaOrdenadaPorNomeDaUnidadeFederativa() {
repositorio.incluir(criar("11111111111", "Ana", "3550308"));
repositorio.incluir(criar("22222222222", "Maria", "3304557"));
var resultado = repositorioAplicacao.pesquisar(consultar(ClienteOrdenacao.UNIDADE_FEDERATIVA_NOME,
Optional.empty(), Optional.empty(), Optional.empty()));
assertEquals("Maria", resultado.getProjecoes().get(0).getNome());
assertEquals("Ana", resultado.getProjecoes().get(1).getNome());
}
@Test
void pesquisaFiltrandoPorTrechoDoNome() {
repositorio.incluir(criar("11111111111", "Maria", "3550308"));
repositorio.incluir(criar("22222222222", "Ana", "3304557"));
var resultado = repositorioAplicacao
.pesquisar(consultar(Optional.of("mar"), Optional.empty(), Optional.empty()));
assertEquals(1, resultado.getTotal());
assertEquals("Maria", resultado.getProjecoes().get(0).getNome());
}
@Test
void pesquisaFiltrandoPorMunicipio() {
repositorio.incluir(criar("11111111111", "Maria", "3550308"));
repositorio.incluir(criar("22222222222", "Ana", "3304557"));
var resultado = repositorioAplicacao
.pesquisar(consultar(Optional.empty(), Optional.of("3304557"), Optional.empty()));
assertEquals(1, resultado.getTotal());
assertEquals("Ana", resultado.getProjecoes().get(0).getNome());
}
@Test
void pesquisaFiltrandoPorUnidadeFederativa() {
repositorio.incluir(criar("11111111111", "Maria", "3550308"));
repositorio.incluir(criar("22222222222", "Ana", "3548500"));
repositorio.incluir(criar("33333333333", "Beatriz", "3304557"));
var resultado = repositorioAplicacao
.pesquisar(consultar(Optional.empty(), Optional.empty(), Optional.of("35")));
assertEquals(2, resultado.getTotal());
assertEquals("Ana", resultado.getProjecoes().get(0).getNome());
assertEquals("Maria", resultado.getProjecoes().get(1).getNome());
}
}
Acrescente aos testes do repositório de município a verificação de existeReferencia, persistindo um cliente diretamente através de IClienteRepositorioJpa:
package com.example.exemplo.infraestrutura.geografia.municipio;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.infraestrutura.crm.cliente.ClienteJpa;
import com.example.exemplo.infraestrutura.crm.cliente.IClienteRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.IUnidadeFederativaRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.UnidadeFederativaJpa;
import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;
@DataJpaTest
class MunicipioRepositorioTest {
private static final UnidadeFederativaCodigoIbge SAO_PAULO = new UnidadeFederativaCodigoIbge("35");
private static final UnidadeFederativaCodigoIbge RIO_DE_JANEIRO = new UnidadeFederativaCodigoIbge("33");
@Autowired
private IMunicipioRepositorioJpa repositorioJpa;
@Autowired
private IUnidadeFederativaRepositorioJpa unidadeFederativaRepositorioJpa;
@Autowired
private IClienteRepositorioJpa clienteRepositorioJpa;
private MunicipioRepositorio repositorio;
@BeforeEach
void configurar() {
repositorio = new MunicipioRepositorio(repositorioJpa, unidadeFederativaRepositorioJpa);
unidadeFederativaRepositorioJpa.save(new UnidadeFederativaJpa("35", "SP", "São Paulo"));
}
private static Municipio criar(String codigoIbge, String nome) {
return new Municipio(new MunicipioCodigoIbge(codigoIbge), nome, Optional.empty());
}
@Test
void incluiEBuscaPorIdentificador() {
repositorio.incluir(criar("3550308", "São Paulo"));
var municipio = repositorio.buscarPorIdentificador(new MunicipioCodigoIbge("3550308"));
assertEquals("São Paulo", municipio.getNome());
assertEquals(SAO_PAULO, municipio.getUnidadeFederativa());
assertFalse(municipio.isExtinto());
}
@Test
void buscaPorIdentificadorInexistenteLancaExcecao() {
assertThrows(AgregadoInexistenteExcecao.class,
() -> repositorio.buscarPorIdentificador(new MunicipioCodigoIbge("3550308")));
}
@Test
void existePorIdentificador() {
repositorio.incluir(criar("3550308", "São Paulo"));
assertTrue(repositorio.existePorIdentificador(new MunicipioCodigoIbge("3550308")));
assertFalse(repositorio.existePorIdentificador(new MunicipioCodigoIbge("3304557")));
}
@Test
void alteraPreservandoExtincao() {
repositorio.incluir(criar("3550308", "São Paulo").extinguir());
repositorio.alterar(new Municipio(new MunicipioCodigoIbge("3550308"), "São Paulo Alterado", Optional.of(true)));
var municipio = repositorio.buscarPorIdentificador(new MunicipioCodigoIbge("3550308"));
assertEquals("São Paulo Alterado", municipio.getNome());
assertTrue(municipio.isExtinto());
}
@Test
void exclui() {
repositorio.incluir(criar("3550308", "São Paulo"));
repositorio.excluir(new MunicipioCodigoIbge("3550308"));
assertFalse(repositorio.existePorIdentificador(new MunicipioCodigoIbge("3550308")));
}
@Test
void existePorNomeNaMesmaUnidadeFederativa() {
repositorio.incluir(criar("3550308", "São Paulo"));
assertTrue(repositorio.existePorNome("São Paulo", SAO_PAULO, Optional.empty()));
assertFalse(repositorio.existePorNome("São Paulo", RIO_DE_JANEIRO, Optional.empty()));
assertFalse(repositorio.existePorNome("Santos", SAO_PAULO, Optional.empty()));
}
@Test
void existePorNomeIgnorandoOProprioIdentificador() {
repositorio.incluir(criar("3550308", "São Paulo"));
var identificador = Optional.of(new MunicipioCodigoIbge("3550308"));
assertFalse(repositorio.existePorNome("São Paulo", SAO_PAULO, identificador));
}
@Test
void existePorNomeNaoIgnorandoUmMunicipioDiferente() {
repositorio.incluir(criar("3550308", "São Paulo"));
repositorio.incluir(criar("3548500", "Santos"));
var identificador = Optional.of(new MunicipioCodigoIbge("3548500"));
assertTrue(repositorio.existePorNome("São Paulo", SAO_PAULO, identificador));
}
@Test
void existePorNomeIgnorandoMaiusculasEMinusculas() {
repositorio.incluir(criar("3550308", "São Paulo"));
assertTrue(repositorio.existePorNome("SÃO PAULO", SAO_PAULO, Optional.empty()));
}
@Test
void existeReferenciaPorCliente() {
repositorio.incluir(criar("3550308", "São Paulo"));
repositorio.incluir(criar("3548500", "Santos"));
clienteRepositorioJpa.save(new ClienteJpa(null, "11111111111", "Maria", "Avenida Paulista", "1000", null,
"Bela Vista", "01310100", repositorioJpa.getReferenceById("3550308")));
assertTrue(repositorio.existeReferencia(new MunicipioCodigoIbge("3550308")));
assertFalse(repositorio.existeReferencia(new MunicipioCodigoIbge("3548500")));
}
}
Execute os testes da infraestrutura:
mvn -pl infraestrutura -am test
UI
O cliente é exposto no pacote com.example.exemplo.ui.backend.crm.cliente, com o caminho crm/cliente. Crie o package-info.java desse pacote, anotado com @NullMarked, e o controlador, como o da unidade federativa:
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.crm.cliente;
package com.example.exemplo.ui.backend.crm.cliente;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.exemplo.aplicacao.crm.cliente.ClienteAplicacaoServico;
import com.example.exemplo.aplicacao.crm.cliente.ClienteConsulta;
import com.example.exemplo.aplicacao.crm.cliente.ClienteProjecao;
import com.example.exemplo.dominio.crm.cliente.Cliente;
import com.example.exemplo.dominio.crm.cliente.ClienteIdentificador;
import com.example.exemplo.dominio.crm.cliente.ClienteServico;
import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;
import dev.sauloaraujo.d5.framework.ui.AgregadoControladorAbstrato;
import dev.sauloaraujo.d5.framework.ui.Backend;
@RestController
@RequestMapping(Backend.PREFIXO + "crm/cliente")
public class ClienteControlador extends
AgregadoControladorAbstrato<ClienteIdentificador, Cliente, AgregadoAutorizacoes<Cliente>, ClienteProjecao, ClienteConsulta> {
public ClienteControlador(ClienteServico servico, ClienteAplicacaoServico servicoAplicacao) {
super(servico, servicoAplicacao);
}
}
Autenticação
Como o município na lição 2, o cliente não precisa entrar no recurso de autenticação: não altere AutenticacaoRecurso nem AutenticacaoControlador.
Testando com o Swagger UI
Suba novamente BackendAplicacao, como na lição 2 — pela linha de comando, executando de novo o mvn install antes do spring-boot:run —, acesse o Swagger UI e autentique-se como na lição 1. As operações de cliente aparecem sob a tag cliente-controlador:

Como o banco de dados em memória é recriado a cada inicialização, inclua de novo a unidade federativa e o município de São Paulo, como nas lições anteriores — sem o município, a inclusão do cliente responde 404. Em seguida, inclua um cliente através de POST /backend/crm/cliente/incluir. O corpo de exemplo sugerido pelo Swagger UI traz o atributo identificador: remova-o, porque o identificador é gerado na inclusão e, se informado, a inclusão responde 422. O complemento do endereço é opcional e também pode ser removido:
{
"cpf": "11111111111",
"nome": "Maria",
"endereco": {
"logradouro": "Avenida Paulista",
"numero": "1000",
"bairro": "Bela Vista",
"cep": "01310100",
"municipio": "3550308"
}
}

GET /backend/crm/cliente/pesquisa devolve, para cada cliente, o identificador gerado na inclusão e os nomes do município e da unidade federativa:

Tente, então, excluir o município de São Paulo: como ele é referenciado pelo cliente, a exclusão responde 409, com a mensagem O município 3550308 é referenciado e não pode ser excluído. Do mesmo modo, incluir um cliente em um município extinto responde 409, com a mensagem O município ... está extinto.
