Pular para conteúdo

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:

exemplo/dominio-crm/pom.xml
<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:

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/package-info.java
@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.

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/ClienteIdentificador.java
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.

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/Cpf.java
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);
    }
}
exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/EnderecoCep.java
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.

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/Endereco.java
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.

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/Cliente.java
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:

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/spi/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.crm.cliente.spi;
exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/spi/IClienteRepositorio.java
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.

exemplo/dominio-crm/src/main/java/com/example/exemplo/dominio/crm/cliente/ClienteServico.java
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:

exemplo/dominio-crm/src/test/java/com/example/exemplo/dominio/crm/CucumberTests.java
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:

exemplo/dominio-crm/src/test/resources/features/cliente.feature
# 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.

exemplo/dominio-crm/src/test/java/com/example/exemplo/dominio/crm/cliente/ClienteStepDefinitions.java
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:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/spi/IMunicipioRepositorio.java
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:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/MunicipioServico.java
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:

exemplo/dominio-geografia/src/test/resources/features/municipio.feature
# 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
exemplo/dominio-geografia/src/test/java/com/example/exemplo/dominio/geografia/municipio/MunicipioStepDefinitions.java
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:

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/package-info.java
@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.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/ClienteProjecao.java
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.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/ClienteOrdenacao.java
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.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/ClienteConsulta.java
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:

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/spi/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.crm.cliente.spi;
exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/spi/IClienteAplicacaoRepositorio.java
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> {
}
exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/crm/cliente/ClienteAplicacaoServico.java
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:

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/crm/cliente/package-info.java
@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.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/crm/cliente/ClienteJpa.java
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.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/crm/cliente/IClienteRepositorioJpa.java
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.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/crm/cliente/ClienteRepositorio.java
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.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/crm/cliente/ClienteAplicacaoRepositorio.java
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:

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/municipio/IMunicipioRepositorioJpa.java
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);
}
exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/municipio/MunicipioRepositorio.java
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:

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/crm/cliente/ClienteRepositorioTest.java
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.

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/crm/cliente/ClienteAplicacaoRepositorioTest.java
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:

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/geografia/municipio/MunicipioRepositorioTest.java
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:

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/crm/cliente/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.crm.cliente;
exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/crm/cliente/ClienteControlador.java
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:

Operações de ClienteControlador no Swagger UI

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"
  }
}

Corpo da requisição de inclusão preenchido com o cliente Maria

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:

Resposta 200 da pesquisa de clientes, com 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.

Resposta 409 da exclusão do município referenciado pelo cliente