Pular para conteúdo

Lição 2: município

Nesta lição, você vai acrescentar ao subdomínio Geografia o agregado município, que referencia a unidade federativa criada na lição 1. Ao final, o projeto corresponde ao diretório exemplo2 do repositório de D5.

O município segue a mesma estrutura da unidade federativa — objetos de valor, agregado, repositório, serviço, projeção, consulta, ordenação, entidade JPA e controlador —, por isso esta lição explica apenas o que muda:

  • o identificador do município contém o identificador da unidade federativa;
  • o município referencia a unidade federativa pelo identificador, e não por um objeto;
  • a entidade JPA do município, por sua vez, referencia a da unidade federativa por uma associação @ManyToOne;
  • o serviço valida a existência de outro agregado e oferece uma ação além das quatro operações básicas: extinguir;
  • a projeção traz o nome da unidade federativa, obtido por junção na consulta JPQL;
  • a unidade federativa passa a ser referenciada e, por isso, não pode mais ser excluída enquanto houver municípios nela.

Município

Um município é identificado por um código IBGE de sete dígitos e possui um nome único na unidade federativa a que pertence. Um município pode ser extinto, o que o mantém cadastrado, mas impede que seja alterado. Seguindo as convenções, o pacote do agregado é com.example.exemplo.dominio.geografia.municipio. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.geografia.municipio;

Domínio

Código IBGE

Os dois primeiros dígitos do código IBGE de um município são o código IBGE da unidade federativa a que ele pertence. Por isso, além de validar o formato com ObjetoValorRegexAbstrato e identificar o município com IIdentificador, o objeto de valor expõe getUnidadeFederativa, que deriva o código IBGE da unidade federativa desses dois dígitos.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/MunicipioCodigoIbge.java
package com.example.exemplo.dominio.geografia.municipio;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;

import dev.sauloaraujo.d5.framework.dominio.IIdentificador;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorRegexAbstrato;

public final class MunicipioCodigoIbge extends ObjetoValorRegexAbstrato implements IIdentificador {
    public static final int TAMANHO = 7;

    private static final String FORMATO = "\\d{" + TAMANHO + "}";
    private static final String MENSAGEM = "O código IBGE do município deve conter " + TAMANHO + " dígitos";

    public MunicipioCodigoIbge(String valor) {
        super(valor, FORMATO, MENSAGEM);
    }

    public UnidadeFederativaCodigoIbge getUnidadeFederativa() {
        return new UnidadeFederativaCodigoIbge(toString().substring(0, UnidadeFederativaCodigoIbge.TAMANHO));
    }
}

Agregado

O município referencia a unidade federativa apenas pelo seu identificador, derivado do código IBGE, e não por um objeto UnidadeFederativa: cada agregado é carregado, validado e persistido isoladamente, e um agregado só conhece os demais pelos seus identificadores.

O indicador de extinção é recebido no construtor como Optional<Boolean> — um município incluído sem esse indicador não está extinto. Um município só se torna extinto através de extinguir, que devolve um novo município, em vez de alterar o atual, e, validando o estado do município, rejeita a extinção de um município já extinto.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/Municipio.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.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;

import dev.sauloaraujo.d5.framework.dominio.IAgregado;

public final class Municipio implements IAgregado<MunicipioCodigoIbge> {
    public static final String EXTINTO_MODELO = "O município %s está extinto";

    private final MunicipioCodigoIbge identificador;
    private final String nome;
    private final boolean extinto;

    public Municipio(MunicipioCodigoIbge identificador, String nome, Optional<Boolean> extinto) {
        this.identificador = Objects.requireNonNull(identificador);
        this.nome = Validate.notBlank(nome, "Nome do município em branco");
        this.extinto = Objects.requireNonNull(extinto).orElse(false);
    }

    public Municipio extinguir() {
        Validate.validState(!extinto, EXTINTO_MODELO, identificador);

        return new Municipio(identificador, nome, Optional.of(true));
    }

    @Override
    public Optional<MunicipioCodigoIbge> getIdentificador() {
        return Optional.of(identificador);
    }

    public String getNome() {
        return nome;
    }

    public UnidadeFederativaCodigoIbge getUnidadeFederativa() {
        return identificador.getUnidadeFederativa();
    }

    public boolean isExtinto() {
        return extinto;
    }
}

Repositório

O nome de um município é único apenas na unidade federativa a que ele pertence. Por isso, existePorNome recebe, além do nome e do identificador a ignorar, a unidade federativa em que a verificação é feita. Crie o pacote spi e seu package-info.java, anotado com @NullMarked:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/spi/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.dominio.geografia.municipio.spi;
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);
}

Autorizações

AgregadoAutorizacoes representa apenas as quatro operações básicas. Como o município oferece também a ação de extinguir, suas autorizações estendem essa classe, acrescentando a autorização extinguir.

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/municipio/MunicipioAutorizacoes.java
package com.example.exemplo.dominio.geografia.municipio;

import dev.sauloaraujo.d5.framework.dominio.impl.AgregadoAutorizacoes;

public final class MunicipioAutorizacoes extends AgregadoAutorizacoes<Municipio> {
    private final boolean extinguir;

    public MunicipioAutorizacoes(boolean incluir, boolean buscar, boolean alterar, boolean excluir, boolean extinguir) {
        super(incluir, buscar, alterar, excluir);

        this.extinguir = extinguir;
    }

    public boolean getExtinguir() {
        return extinguir;
    }
}

Serviço

Como na unidade federativa, o serviço estende AgregadoServicoAbstrato, agora parametrizado por MunicipioAutorizacoes, e libera a busca a qualquer usuário e as demais operações — inclusive extinguir — somente a administradores. As autorizações dependem apenas do usuário, e não do estado do município: um administrador continua autorizado a alterar e a extinguir um município extinto, e é o serviço que rejeita essas operações, validando o estado do município. Autorização e estado são coisas distintas — a primeira diz quem pode executar uma operação; o segundo, se a operação é possível —, e por isso a rejeição de uma operação sobre um município extinto responde 409, e não 403. Além de IMunicipioRepositorio, ele recebe IUnidadeFederativaRepositorio, para verificar a existência da unidade federativa do município. Em validarInclusao e validarAlteracao, o serviço valida o argumento, rejeitando um município recebido já extinto, que só pode se tornar extinto pela ação de extinguir. Em validarInclusaoAlteracao, o serviço:

  • usa validarExistencia, com o modelo de mensagem de UnidadeFederativaServico, para exigir que a unidade federativa exista — caso contrário, lança AgregadoInexistenteExcecao, e a requisição responde 404, como quando o próprio município não existe;
  • usa validarInexistencia e existePorNome para exigir que o nome seja único na unidade federativa.

Em validarAlteracao, depois de validar o argumento, o serviço também rejeita a alteração de um município extinto, validando o estado do município existente. A regra sobre o município recebido não fica em validarInclusaoAlteracao por causa da ordem das validações de AgregadoServicoAbstrato: validarInclusao é chamado antes da verificação de inexistência do identificador, e validarAlteracao, depois da verificação de existência do município, mas ambos antes de validarInclusaoAlteracao. Assim, a inclusão de um município extinto é rejeitada como extinção indevida mesmo que seu código já exista, e a alteração de um município extinto é rejeitada porque ele está extinto mesmo que o nome recebido já seja usado na unidade federativa. Já a alteração de um município inexistente responde 404, ainda que ele seja recebido extinto. Por fim, extinguir valida a autorização extinguir com validarAutorizacao e a existência do município com validarExistencia — ambos herdados de AgregadoServicoAbstrato — e persiste o município devolvido por Municipio.extinguir.

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

    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);
    }

    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());
    }
}

Testes

Escreva os cenários cobrindo, além da inclusão, da busca, da alteração e da exclusão, a ação de extinguir e as regras de existência da unidade federativa, de unicidade do nome na unidade federativa e de imutabilidade de um município extinto:

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: 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

As step definitions dublam, além da segurança e do repositório de municípios, o repositório de unidades federativas, que simula a existência das unidades federativas através de um conjunto de identificadores. A suíte do Cucumber do subdomínio, criada na lição 1, já seleciona os novos cenários.

Os passos, porém, são compartilhados por todos os cenários do subdomínio: a suíte procura as step definitions em todas as classes do pacote com.example.exemplo.dominio.geografia e de seus subpacotes, e duas expressões iguais, ainda que em classes diferentes, interrompem os cenários que as usam com uma DuplicateStepDefinitionException. Por isso, os passos do município têm textos próprios — por exemplo, que sou um administrador do cadastro de municípios, e não que sou um usuário administrador, já definido nas step definitions da unidade federativa.

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 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.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();
    }

    @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);
    }

    @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("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("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());
    }
}

Unidade federativa referenciada

Agora que municípios referenciam unidades federativas, excluir uma unidade federativa com municípios deixaria esses municípios órfãos. Para impedir isso, o repositório de unidade federativa passa a expor existeReferencia, que informa se algum outro agregado referencia a unidade federativa. Quais agregados são esses é um detalhe da implementação do repositório: o serviço de unidade federativa não precisa conhecer o município, e continuará sem conhecer outros agregados que venham a referenciá-la.

Acrescente existeReferencia à interface do repositório:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/spi/IUnidadeFederativaRepositorio.java
package com.example.exemplo.dominio.geografia.unidadefederativa.spi;

import java.util.Optional;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;

import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;

public interface IUnidadeFederativaRepositorio
        extends IAgregadoRepositorio<UnidadeFederativaCodigoIbge, UnidadeFederativa> {
    boolean existePorSigla(UnidadeFederativaSigla sigla, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado);

    boolean existePorNome(String nome, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado);

    boolean existeReferencia(UnidadeFederativaCodigoIbge identificador);
}

Em UnidadeFederativaServico, sobrescreva validarExclusao para usar existeReferencia e, validando o estado da aplicação, impedir a exclusão de uma unidade federativa referenciada:

exemplo/dominio-geografia/src/main/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativaServico.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

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.unidadefederativa.spi.IUnidadeFederativaRepositorio;

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 UnidadeFederativaServico extends
        AgregadoServicoAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, AgregadoAutorizacoes<UnidadeFederativa>> {
    public static final String IDENTIFICADOR_INEXISTENTE_MODELO = "Não existe unidade federativa com o identificador %s";
    public static final String IDENTIFICADOR_EXISTENTE_MODELO = "Já existe uma unidade federativa com o identificador %s";
    public static final String SIGLA_EXISTENTE_MODELO = "Já existe uma unidade federativa com a sigla %s";
    public static final String NOME_EXISTENTE_MODELO = "Já existe uma unidade federativa com o nome %s";
    public static final String REFERENCIADA_MODELO = "A unidade federativa %s é referenciada e não pode ser excluída";

    private final IUnidadeFederativaRepositorio repositorio;

    public UnidadeFederativaServico(ISegurancaServico segurancaServico, IUnidadeFederativaRepositorio repositorio) {
        super(segurancaServico, repositorio, IDENTIFICADOR_EXISTENTE_MODELO, IDENTIFICADOR_INEXISTENTE_MODELO);

        this.repositorio = Objects.requireNonNull(repositorio);
    }

    @Override
    public AgregadoAutorizacoes<UnidadeFederativa> criarAutorizacoes(
            Optional<UnidadeFederativaCodigoIbge> identificador) {
        Objects.requireNonNull(identificador);

        var administrador = getSegurancaServico().possuiAutorizacao(Autorizacao.ADMINISTRADOR.name());
        return new AgregadoAutorizacoes<>(administrador, true, administrador, administrador);
    }

    @Override
    protected void validarInclusaoAlteracao(UnidadeFederativa agregado) {
        Objects.requireNonNull(agregado);

        var identificador = agregado.getIdentificador();

        validarInexistencia(sigla -> repositorio.existePorSigla(sigla, identificador), agregado.getSigla(),
                SIGLA_EXISTENTE_MODELO);

        validarInexistencia(nome -> repositorio.existePorNome(nome, identificador), agregado.getNome(),
                NOME_EXISTENTE_MODELO);
    }

    @Override
    protected void validarExclusao(UnidadeFederativaCodigoIbge identificador) {
        Objects.requireNonNull(identificador);

        Validate.validState(!repositorio.existeReferencia(identificador), REFERENCIADA_MODELO, identificador);
    }
}

Acrescente aos cenários da unidade federativa a exclusão de uma unidade federativa referenciada e, às step definitions, a simulação das unidades federativas referenciadas, através de um conjunto de identificadores, e o passo que verifica que a unidade federativa continua a existir depois da exclusão rejeitada:

exemplo/dominio-geografia/src/test/resources/features/unidade-federativa.feature
# language: pt
Funcionalidade: Gerenciar unidades federativas
  Para manter o cadastro de unidades federativas da aplicação
  Como usuário do sistema
  Quero incluir, buscar, alterar e excluir unidades federativas,
  respeitando as regras de unicidade de sigla e nome e de autorização

  Contexto:
    Dado que não existe nenhuma unidade federativa cadastrada

  Cenário: Incluir uma unidade federativa
    Dado que sou um usuário administrador
    Quando eu incluo a unidade federativa de código IBGE "35", sigla "SP" e nome "São Paulo"
    Então a unidade federativa é incluída com sucesso
    E a unidade federativa de código IBGE "35" passa a existir

  Cenário: Não é possível incluir uma unidade federativa sem autorização de administrador
    Dado que sou um usuário sem autorização de administrador
    Quando eu tento incluir a unidade federativa de código IBGE "35", sigla "SP" e nome "São Paulo"
    Então a inclusão é rejeitada por falta de autorização

  Cenário: Não é possível incluir uma unidade federativa com código IBGE já existente
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "35", sigla "RJ" e nome "Rio de Janeiro"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com o identificador "35"

  Cenário: Não é possível incluir uma unidade federativa com sigla já existente
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "SP" e nome "Rio de Janeiro"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com a sigla "SP"

  Cenário: Não é possível incluir uma unidade federativa com nome já existente
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "RJ" e nome "São Paulo"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com o nome "São Paulo"

  Cenário: Não é possível incluir uma unidade federativa com nome já existente, ainda que com outras maiúsculas e minúsculas
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento incluir a unidade federativa de código IBGE "33", sigla "RJ" e nome "SÃO PAULO"
    Então a inclusão é rejeitada porque já existe uma unidade federativa com o nome "SÃO PAULO"

  Cenário: Buscar uma unidade federativa existente
    Dado que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    E que sou um usuário sem autorização de administrador
    Quando eu busco a unidade federativa de código IBGE "35"
    Então a unidade federativa encontrada tem sigla "SP" e nome "São Paulo"

  Cenário: Não é possível buscar uma unidade federativa inexistente
    Dado que sou um usuário sem autorização de administrador
    Quando eu tento buscar a unidade federativa de código IBGE "35"
    Então a busca é rejeitada porque não existe unidade federativa com o identificador "35"

  Cenário: Alterar uma unidade federativa
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu altero a unidade federativa de código IBGE "35" para sigla "SP" e nome "Estado de São Paulo"
    Então a unidade federativa é alterada com sucesso
    E a unidade federativa de código IBGE "35" passa a ter sigla "SP" e nome "Estado de São Paulo"

  Cenário: Alterar uma unidade federativa mantendo sua própria sigla e nome
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu altero a unidade federativa de código IBGE "35" para sigla "SP" e nome "São Paulo"
    Então a unidade federativa é alterada com sucesso

  Cenário: Não é possível alterar uma unidade federativa sem autorização de administrador
    Dado que sou um usuário sem autorização de administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento alterar a unidade federativa de código IBGE "35" para sigla "SP" e nome "Estado de São Paulo"
    Então a alteração é rejeitada por falta de autorização

  Cenário: Não é possível alterar uma unidade federativa inexistente
    Dado que sou um usuário administrador
    Quando eu tento alterar a unidade federativa de código IBGE "35" para sigla "SP" e nome "São Paulo"
    Então a alteração é rejeitada porque não existe unidade federativa com o identificador "35"

  Cenário: Não é possível alterar uma unidade federativa para a sigla de outra
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    E que existe uma unidade federativa com código IBGE "33", sigla "RJ" e nome "Rio de Janeiro"
    Quando eu tento alterar a unidade federativa de código IBGE "33" para sigla "SP" e nome "Rio de Janeiro"
    Então a alteração é rejeitada porque já existe uma unidade federativa com a sigla "SP"

  Cenário: Excluir uma unidade federativa
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu excluo a unidade federativa de código IBGE "35"
    Então a unidade federativa é excluída com sucesso
    E a unidade federativa de código IBGE "35" deixa de existir

  Cenário: Não é possível excluir uma unidade federativa sem autorização de administrador
    Dado que sou um usuário sem autorização de administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    Quando eu tento excluir a unidade federativa de código IBGE "35"
    Então a exclusão é rejeitada por falta de autorização

  Cenário: Não é possível excluir uma unidade federativa inexistente
    Dado que sou um usuário administrador
    Quando eu tento excluir a unidade federativa de código IBGE "35"
    Então a exclusão é rejeitada porque não existe unidade federativa com o identificador "35"

  Cenário: Não é possível excluir uma unidade federativa referenciada
    Dado que sou um usuário administrador
    E que existe uma unidade federativa com código IBGE "35", sigla "SP" e nome "São Paulo"
    E que a unidade federativa de código IBGE "35" é referenciada
    Quando eu tento excluir a unidade federativa de código IBGE "35"
    Então a exclusão é rejeitada porque a unidade federativa "35" é referenciada
    E a unidade federativa de código IBGE "35" continua a existir
exemplo/dominio-geografia/src/test/java/com/example/exemplo/dominio/geografia/unidadefederativa/UnidadeFederativaStepDefinitions.java
package com.example.exemplo.dominio.geografia.unidadefederativa;

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.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 UnidadeFederativaStepDefinitions {
    private final Map<String, UnidadeFederativa> unidadesFederativas = new HashMap<>();
    private final Set<String> referenciadas = new HashSet<>();
    private final ISegurancaServico segurancaServico = mock(ISegurancaServico.class);
    private final IUnidadeFederativaRepositorio repositorio = mock(IUnidadeFederativaRepositorio.class);
    private final UnidadeFederativaServico servico = new UnidadeFederativaServico(segurancaServico, repositorio);

    private UnidadeFederativa resultadoInclusao;
    private UnidadeFederativa resultadoBusca;
    private RuntimeException excecaoCapturada;

    public UnidadeFederativaStepDefinitions() {
        when(repositorio.existePorIdentificador(any()))
                .thenAnswer(chamada -> unidadesFederativas.containsKey(chave(chamada.getArgument(0))));

        when(repositorio.buscarPorIdentificador(any())).thenAnswer(chamada -> {
            UnidadeFederativaCodigoIbge codigoIbge = chamada.getArgument(0);
            var unidadeFederativa = unidadesFederativas.get(chave(codigoIbge));
            if (unidadeFederativa == null) {
                throw new AgregadoInexistenteExcecao(
                        String.format(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, codigoIbge));
            }
            return unidadeFederativa;
        });

        when(repositorio.existePorSigla(any(), any())).thenAnswer(chamada -> {
            UnidadeFederativaSigla sigla = chamada.getArgument(0);
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado = chamada.getArgument(1);
            return unidadesFederativas.values().stream().anyMatch(
                    candidata -> candidata.getSigla().equals(sigla) && !foiIgnorada(candidata, identificadorIgnorado));
        });

        when(repositorio.existePorNome(any(), any())).thenAnswer(chamada -> {
            String nome = chamada.getArgument(0);
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado = chamada.getArgument(1);
            return unidadesFederativas.values().stream()
                    .anyMatch(candidata -> candidata.getNome().equalsIgnoreCase(nome)
                            && !foiIgnorada(candidata, identificadorIgnorado));
        });

        when(repositorio.existeReferencia(any()))
                .thenAnswer(chamada -> referenciadas.contains(chave(chamada.getArgument(0))));

        when(repositorio.incluir(any())).thenAnswer(chamada -> {
            UnidadeFederativa unidadeFederativa = chamada.getArgument(0);
            unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
            return unidadeFederativa;
        });

        doAnswer(chamada -> {
            UnidadeFederativa unidadeFederativa = chamada.getArgument(0);
            unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
            return null;
        }).when(repositorio).alterar(any());

        doAnswer(chamada -> {
            UnidadeFederativaCodigoIbge codigoIbge = chamada.getArgument(0);
            unidadesFederativas.remove(chave(codigoIbge));
            return null;
        }).when(repositorio).excluir(any());
    }

    private static String chave(UnidadeFederativaCodigoIbge codigoIbge) {
        return codigoIbge.toString();
    }

    private static boolean foiIgnorada(UnidadeFederativa candidata,
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
        return identificadorIgnorado.isPresent() && candidata.getIdentificador().equals(identificadorIgnorado);
    }

    private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
                nome);
    }

    @Dado("que não existe nenhuma unidade federativa cadastrada")
    public void que_nao_existe_nenhuma_unidade_federativa_cadastrada() {
        unidadesFederativas.clear();
        referenciadas.clear();
    }

    @Dado("que sou um usuário administrador")
    public void que_sou_um_usuario_administrador() {
        when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(true);
    }

    @Dado("que sou um usuário sem autorização de administrador")
    public void que_sou_um_usuario_sem_autorizacao_de_administrador() {
        when(segurancaServico.possuiAutorizacao(Autorizacao.ADMINISTRADOR.name())).thenReturn(false);
    }

    @Dado("que existe uma unidade federativa com código IBGE {string}, sigla {string} e nome {string}")
    public void que_existe_uma_unidade_federativa(String codigoIbge, String sigla, String nome) {
        var unidadeFederativa = criar(codigoIbge, sigla, nome);
        unidadesFederativas.put(chave(unidadeFederativa.getIdentificador().orElseThrow()), unidadeFederativa);
    }

    @Dado("que a unidade federativa de código IBGE {string} é referenciada")
    public void que_a_unidade_federativa_e_referenciada(String codigoIbge) {
        referenciadas.add(codigoIbge);
    }

    @Quando("eu incluo a unidade federativa de código IBGE {string}, sigla {string} e nome {string}")
    public void eu_incluo_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        incluir(codigoIbge, sigla, nome);
    }

    @Quando("eu tento incluir a unidade federativa de código IBGE {string}, sigla {string} e nome {string}")
    public void eu_tento_incluir_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        incluir(codigoIbge, sigla, nome);
    }

    private void incluir(String codigoIbge, String sigla, String nome) {
        try {
            resultadoInclusao = servico.incluir(criar(codigoIbge, sigla, nome));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Quando("eu busco a unidade federativa de código IBGE {string}")
    public void eu_busco_a_unidade_federativa(String codigoIbge) {
        buscar(codigoIbge);
    }

    @Quando("eu tento buscar a unidade federativa de código IBGE {string}")
    public void eu_tento_buscar_a_unidade_federativa(String codigoIbge) {
        buscar(codigoIbge);
    }

    private void buscar(String codigoIbge) {
        try {
            resultadoBusca = servico.buscar(new UnidadeFederativaCodigoIbge(codigoIbge));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Quando("eu altero a unidade federativa de código IBGE {string} para sigla {string} e nome {string}")
    public void eu_altero_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        alterar(codigoIbge, sigla, nome);
    }

    @Quando("eu tento alterar a unidade federativa de código IBGE {string} para sigla {string} e nome {string}")
    public void eu_tento_alterar_a_unidade_federativa(String codigoIbge, String sigla, String nome) {
        alterar(codigoIbge, sigla, nome);
    }

    private void alterar(String codigoIbge, String sigla, String nome) {
        try {
            servico.alterar(criar(codigoIbge, sigla, nome));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Quando("eu excluo a unidade federativa de código IBGE {string}")
    public void eu_excluo_a_unidade_federativa(String codigoIbge) {
        excluir(codigoIbge);
    }

    @Quando("eu tento excluir a unidade federativa de código IBGE {string}")
    public void eu_tento_excluir_a_unidade_federativa(String codigoIbge) {
        excluir(codigoIbge);
    }

    private void excluir(String codigoIbge) {
        try {
            servico.excluir(new UnidadeFederativaCodigoIbge(codigoIbge));
        } catch (RuntimeException excecao) {
            excecaoCapturada = excecao;
        }
    }

    @Entao("a unidade federativa é incluída com sucesso")
    public void a_unidade_federativa_e_incluida_com_sucesso() {
        assertNull(excecaoCapturada);
        assertNotNull(resultadoInclusao);
    }

    @Entao("a unidade federativa de código IBGE {string} passa a existir")
    public void a_unidade_federativa_passa_a_existir(String codigoIbge) {
        assertTrue(unidadesFederativas.containsKey(codigoIbge));
    }

    @Entao("a unidade federativa de código IBGE {string} continua a existir")
    public void a_unidade_federativa_continua_a_existir(String codigoIbge) {
        assertTrue(unidadesFederativas.containsKey(codigoIbge));
    }

    @Entao("a inclusão é 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 uma unidade federativa com o identificador {string}")
    public void a_inclusao_e_rejeitada_pelo_identificador(String codigoIbge) {
        assertMensagemDeExistencia(UnidadeFederativaServico.IDENTIFICADOR_EXISTENTE_MODELO, codigoIbge);
    }

    @Entao("a inclusão é rejeitada porque já existe uma unidade federativa com a sigla {string}")
    public void a_inclusao_e_rejeitada_pela_sigla(String sigla) {
        assertMensagemDeExistencia(UnidadeFederativaServico.SIGLA_EXISTENTE_MODELO, sigla);
    }

    @Entao("a inclusão é rejeitada porque já existe uma unidade federativa com o nome {string}")
    public void a_inclusao_e_rejeitada_pelo_nome(String nome) {
        assertMensagemDeExistencia(UnidadeFederativaServico.NOME_EXISTENTE_MODELO, nome);
    }

    @Entao("a unidade federativa encontrada tem sigla {string} e nome {string}")
    public void a_unidade_federativa_encontrada_tem(String sigla, String nome) {
        assertEquals(new UnidadeFederativaSigla(sigla), resultadoBusca.getSigla());
        assertEquals(nome, resultadoBusca.getNome());
    }

    @Entao("a busca é rejeitada porque não existe unidade federativa com o identificador {string}")
    public void a_busca_e_rejeitada_por_inexistencia(String codigoIbge) {
        assertMensagemDeInexistencia(codigoIbge);
    }

    @Entao("a unidade federativa é alterada com sucesso")
    public void a_unidade_federativa_e_alterada_com_sucesso() {
        assertNull(excecaoCapturada);
    }

    @Entao("a unidade federativa de código IBGE {string} passa a ter sigla {string} e nome {string}")
    public void a_unidade_federativa_passa_a_ter(String codigoIbge, String sigla, String nome) {
        var unidadeFederativa = unidadesFederativas.get(codigoIbge);
        assertNotNull(unidadeFederativa);
        assertEquals(new UnidadeFederativaSigla(sigla), unidadeFederativa.getSigla());
        assertEquals(nome, unidadeFederativa.getNome());
    }

    @Entao("a alteração é 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 unidade federativa com o identificador {string}")
    public void a_alteracao_e_rejeitada_por_inexistencia(String codigoIbge) {
        assertMensagemDeInexistencia(codigoIbge);
    }

    @Entao("a alteração é rejeitada porque já existe uma unidade federativa com a sigla {string}")
    public void a_alteracao_e_rejeitada_pela_sigla(String sigla) {
        assertMensagemDeExistencia(UnidadeFederativaServico.SIGLA_EXISTENTE_MODELO, sigla);
    }

    @Entao("a unidade federativa é excluída com sucesso")
    public void a_unidade_federativa_e_excluida_com_sucesso() {
        assertNull(excecaoCapturada);
    }

    @Entao("a unidade federativa de código IBGE {string} deixa de existir")
    public void a_unidade_federativa_deixa_de_existir(String codigoIbge) {
        assertFalse(unidadesFederativas.containsKey(codigoIbge));
    }

    @Entao("a exclusão é 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 unidade federativa com o identificador {string}")
    public void a_exclusao_e_rejeitada_por_inexistencia(String codigoIbge) {
        assertMensagemDeInexistencia(codigoIbge);
    }

    @Entao("a exclusão é rejeitada porque a unidade federativa {string} é referenciada")
    public void a_exclusao_e_rejeitada_por_referencia(String codigoIbge) {
        assertInstanceOf(IllegalStateException.class, excecaoCapturada);
        assertEquals(String.format(UnidadeFederativaServico.REFERENCIADA_MODELO, codigoIbge),
                excecaoCapturada.getMessage());
    }

    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(UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO, argumento),
                excecaoCapturada.getMessage());
    }
}

Execute os cenários, como na lição 1:

mvn -pl dominio-geografia -am test

O resultado deve informar 42 testes, os 17 cenários da unidade federativa e os 25 do município, sem falhas.

Aplicação

As classes de aplicação do município ficam no pacote com.example.exemplo.aplicacao.geografia.municipio. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.geografia.municipio;

Projeção

Uma lista de municípios costuma exibir o nome da unidade federativa de cada um, e não apenas seu código. Por isso, a projeção traz, além do identificador, do nome, do indicador de extinção e do código da unidade federativa, o nome da unidade federativa — que não pertence ao agregado município, mas é obtido pela consulta que instancia a projeção.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/MunicipioProjecao.java
package com.example.exemplo.aplicacao.geografia.municipio;

import java.util.Objects;

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.aplicacao.IProjecao;

public final class MunicipioProjecao implements IProjecao<Municipio> {
    private final MunicipioCodigoIbge identificador;
    private final String nome;
    private final UnidadeFederativaCodigoIbge unidadeFederativa;
    private final String unidadeFederativaNome;
    private final boolean extinto;

    public MunicipioProjecao(MunicipioCodigoIbge identificador, String nome,
            UnidadeFederativaCodigoIbge unidadeFederativa, String unidadeFederativaNome, boolean extinto) {
        this.identificador = Objects.requireNonNull(identificador);
        this.nome = Objects.requireNonNull(nome);
        this.unidadeFederativa = Objects.requireNonNull(unidadeFederativa);
        this.unidadeFederativaNome = Objects.requireNonNull(unidadeFederativaNome);
        this.extinto = extinto;
    }

    public MunicipioProjecao(String identificador, String nome, String unidadeFederativa, String unidadeFederativaNome,
            boolean extinto) {
        this(new MunicipioCodigoIbge(identificador), nome, new UnidadeFederativaCodigoIbge(unidadeFederativa),
                unidadeFederativaNome, extinto);
    }

    public MunicipioCodigoIbge getIdentificador() {
        return identificador;
    }

    public String getNome() {
        return nome;
    }

    public UnidadeFederativaCodigoIbge getUnidadeFederativa() {
        return unidadeFederativa;
    }

    public String getUnidadeFederativaNome() {
        return unidadeFederativaNome;
    }

    public boolean isExtinto() {
        return extinto;
    }
}

Ordenação

Além do identificador e do nome, os municípios podem ser ordenados pelo nome da unidade federativa.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/MunicipioOrdenacao.java
package com.example.exemplo.aplicacao.geografia.municipio;

import dev.sauloaraujo.d5.framework.aplicacao.IOrdenacao;

public enum MunicipioOrdenacao implements IOrdenacao<MunicipioProjecao> {
    IDENTIFICADOR, NOME, UNIDADE_FEDERATIVA_NOME
}

Consulta

A consulta filtra os municípios por um trecho do nome e pelo código da unidade federativa.

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/MunicipioConsulta.java
package com.example.exemplo.aplicacao.geografia.municipio;

import java.util.Objects;
import java.util.Optional;

import dev.sauloaraujo.d5.framework.aplicacao.impl.Consulta;

public final class MunicipioConsulta extends Consulta<MunicipioOrdenacao> {
    private final Optional<String> nome;
    private final Optional<String> unidadeFederativa;

    public MunicipioConsulta(Optional<MunicipioOrdenacao> ordenacao, Optional<Boolean> descendente,
            Optional<Integer> indicePagina, Optional<Integer> tamanhoPagina, Optional<String> nome,
            Optional<String> unidadeFederativa) {
        super(ordenacao, descendente, indicePagina, tamanhoPagina);

        this.nome = Objects.requireNonNull(nome);
        this.unidadeFederativa = Objects.requireNonNull(unidadeFederativa);
    }

    public Optional<String> getNome() {
        return nome;
    }

    public Optional<String> getUnidadeFederativa() {
        return unidadeFederativa;
    }
}

Repositório de aplicação e serviço de aplicação

Como na unidade federativa, crie a interface do repositório de aplicação no pacote spi e o serviço de aplicação, parametrizado agora por MunicipioAutorizacoes:

exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/spi/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.aplicacao.geografia.municipio.spi;
exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/spi/IMunicipioAplicacaoRepositorio.java
package com.example.exemplo.aplicacao.geografia.municipio.spi;

import com.example.exemplo.aplicacao.geografia.municipio.MunicipioConsulta;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioProjecao;

import dev.sauloaraujo.d5.framework.aplicacao.spi.IAgregadoAplicacaoRepositorio;

public interface IMunicipioAplicacaoRepositorio
        extends IAgregadoAplicacaoRepositorio<MunicipioProjecao, MunicipioConsulta> {
}
exemplo/aplicacao/src/main/java/com/example/exemplo/aplicacao/geografia/municipio/MunicipioAplicacaoServico.java
package com.example.exemplo.aplicacao.geografia.municipio;

import com.example.exemplo.aplicacao.geografia.municipio.spi.IMunicipioAplicacaoRepositorio;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioAutorizacoes;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.municipio.MunicipioServico;

import dev.sauloaraujo.d5.framework.aplicacao.impl.AgregadoAplicacaoServicoAbstrato;

public final class MunicipioAplicacaoServico extends
        AgregadoAplicacaoServicoAbstrato<MunicipioCodigoIbge, Municipio, MunicipioAutorizacoes, MunicipioProjecao, MunicipioConsulta> {
    public MunicipioAplicacaoServico(IMunicipioAplicacaoRepositorio repositorio, MunicipioServico servico) {
        super(repositorio, servico);
    }
}

Infraestrutura

A persistência de município fica no pacote com.example.exemplo.infraestrutura.geografia.municipio. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/municipio/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.infraestrutura.geografia.municipio;

Entidade JPA

Diferente do agregado, que referencia a unidade federativa apenas pelo código, a entidade JPA do município a mapeia por uma associação @ManyToOne com UnidadeFederativaJpa, cuja coluna, UFI_ID, é uma chave estrangeira: o banco de dados rejeita, assim, um município de uma unidade federativa inexistente e a exclusão de uma unidade federativa com municípios, ainda que os serviços já impeçam essas situações. A associação é apenas um detalhe da persistência e é preguiçosa (FetchType.LAZY), para que ler um município não carregue a unidade federativa. Como a unidade federativa é derivada do identificador, a coluna também não pode ser alterada. O atributo extinto, mapeado para a coluna MUN_EX, também é anotado com @NotNull: mesmo sendo de um tipo primitivo, que nunca é null, o Hibernate só declara a coluna not null no banco de dados com essa anotação. A restrição de unicidade sobre a unidade federativa e o nome reforça no banco de dados a unicidade validada pelo serviço, mas, como a do nome da unidade federativa, diferencia maiúsculas de minúsculas.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/municipio/MunicipioJpa.java
package com.example.exemplo.infraestrutura.geografia.municipio;

import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.UnidadeFederativaJpa;

import dev.sauloaraujo.d5.framework.infraestrutura.IAgregadoJpa;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

@Entity
@Table(name = "MUN_MUNICIPIO", uniqueConstraints = @UniqueConstraint(columnNames = { "UFI_ID", "MUN_NM" }))
public class MunicipioJpa implements IAgregadoJpa<Municipio> {
    @Id
    @Column(name = "MUN_ID", updatable = false)
    @NotNull
    @Size(max = MunicipioCodigoIbge.TAMANHO)
    private String identificador;

    @Column(name = "MUN_NM")
    @NotNull
    @Size(max = 100)
    private String nome;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "UFI_ID", updatable = false)
    @NotNull
    private UnidadeFederativaJpa unidadeFederativa;

    @Column(name = "MUN_EX")
    @NotNull
    private boolean extinto;

    protected MunicipioJpa() {
    }

    public MunicipioJpa(String identificador, String nome, UnidadeFederativaJpa unidadeFederativa, boolean extinto) {
        this.identificador = identificador;
        this.nome = nome;
        this.unidadeFederativa = unidadeFederativa;
        this.extinto = extinto;
    }

    public String getIdentificador() {
        return identificador;
    }

    public String getNome() {
        return nome;
    }

    public UnidadeFederativaJpa getUnidadeFederativa() {
        return unidadeFederativa;
    }

    public boolean isExtinto() {
        return extinto;
    }
}

Repositório Spring Data

existePorNome restringe a verificação à unidade federativa informada e, como na unidade federativa, compara os nomes em minúsculas. Como a unidade federativa é uma associação, a verificação compara o seu identificador — m.unidadeFederativa.identificador. Já a consulta de pesquisar obtém o nome da unidade federativa através de uma junção pela associação — join m.unidadeFederativa u —, e o filtro pela unidade federativa compara u.identificador. A junção é interna: um município sempre pertence a uma unidade federativa existente, garantia dada pelo serviço de município na inclusão e pelo de unidade federativa na exclusão.

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 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);
}

Implementação do repositório

Para preencher a associação, paraJpa obtém a unidade federativa, derivada do identificador do município, com getReferenceById de IUnidadeFederativaRepositorioJpa, recebido no construtor, que devolve uma referência sem consultar o banco de dados. paraAgregado não precisa ler a associação, já que a unidade federativa é derivada do identificador, e informa explicitamente o indicador de extinção lido do banco de dados.

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);
    }
}

Implementação do repositório de aplicação

Em paraPropriedadeOrdenacao, a ordenação pelo nome da unidade federativa é mapeada para u.nome, qualificada pelo alias da junção declarada na consulta de pesquisar. As demais propriedades, não qualificadas, referem-se ao município.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/municipio/MunicipioAplicacaoRepositorio.java
package com.example.exemplo.infraestrutura.geografia.municipio;

import java.util.Objects;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import com.example.exemplo.aplicacao.geografia.municipio.MunicipioConsulta;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioOrdenacao;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioProjecao;
import com.example.exemplo.aplicacao.geografia.municipio.spi.IMunicipioAplicacaoRepositorio;

import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoAplicacaoRepositorioAbstrato;

public class MunicipioAplicacaoRepositorio
        extends AgregadoAplicacaoRepositorioAbstrato<MunicipioProjecao, MunicipioOrdenacao, MunicipioConsulta>
        implements IMunicipioAplicacaoRepositorio {
    private final IMunicipioRepositorioJpa repositorioJpa;

    public MunicipioAplicacaoRepositorio(IMunicipioRepositorioJpa repositorioJpa) {
        this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
    }

    @Override
    protected Page<MunicipioProjecao> pesquisar(MunicipioConsulta consulta, Pageable pageable) {
        Objects.requireNonNull(consulta);
        Objects.requireNonNull(pageable);

        var nome = paraLike(consulta.getNome());
        var unidadeFederativa = consulta.getUnidadeFederativa().orElse(null);
        return repositorioJpa.pesquisar(nome, unidadeFederativa, pageable);
    }

    @Override
    protected String paraPropriedadeOrdenacao(MunicipioOrdenacao ordenacao) {
        Objects.requireNonNull(ordenacao);

        return switch (ordenacao) {
        case IDENTIFICADOR -> "identificador";
        case NOME -> "nome";
        case UNIDADE_FEDERATIVA_NOME -> "u.nome";
        };
    }
}

Unidade federativa referenciada

Implemente existeReferencia no repositório de unidade federativa. A consulta verifica se algum município referencia a unidade federativa, comparando m.unidadeFederativa.identificador; caso outros agregados passem a referenciá-la, basta acrescentar suas verificações a essa consulta.

exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/IUnidadeFederativaRepositorioJpa.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

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.unidadefederativa.UnidadeFederativaProjecao;

public interface IUnidadeFederativaRepositorioJpa extends JpaRepository<UnidadeFederativaJpa, String> {
    @Query("""
            select count(u) > 0
              from UnidadeFederativaJpa u
             where u.sigla = :sigla
               and (:identificadorIgnorado is null or u.identificador <> :identificadorIgnorado)
            """)
    boolean existePorSigla(String sigla, @Nullable String identificadorIgnorado);

    @Query("""
            select count(u) > 0
              from UnidadeFederativaJpa u
             where lower(u.nome) = lower(:nome)
               and (:identificadorIgnorado is null or u.identificador <> :identificadorIgnorado)
            """)
    boolean existePorNome(String nome, @Nullable String identificadorIgnorado);

    @Query("""
            select count(m) > 0
              from MunicipioJpa m
             where m.unidadeFederativa.identificador = :identificador
            """)
    boolean existeReferencia(String identificador);

    @Query("""
            select new com.example.exemplo.aplicacao.geografia.unidadefederativa.UnidadeFederativaProjecao(u.identificador, u.sigla, u.nome)
              from UnidadeFederativaJpa u
             where (:sigla is null or lower(u.sigla) like :sigla escape '\\')
               and (:nome is null or lower(u.nome) like :nome escape '\\')
            """)
    Page<UnidadeFederativaProjecao> pesquisar(@Nullable String sigla, @Nullable String nome, Pageable pageable);
}
exemplo/infraestrutura/src/main/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaRepositorio.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

import java.util.Objects;
import java.util.Optional;

import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import com.example.exemplo.dominio.geografia.unidadefederativa.spi.IUnidadeFederativaRepositorio;

import dev.sauloaraujo.d5.framework.infraestrutura.AgregadoRepositorioAbstrato;

public class UnidadeFederativaRepositorio extends
        AgregadoRepositorioAbstrato<UnidadeFederativaCodigoIbge, UnidadeFederativa, String, UnidadeFederativaJpa>
        implements IUnidadeFederativaRepositorio {
    private final IUnidadeFederativaRepositorioJpa repositorioJpa;

    public UnidadeFederativaRepositorio(IUnidadeFederativaRepositorioJpa repositorioJpa) {
        super(repositorioJpa, UnidadeFederativaServico.IDENTIFICADOR_INEXISTENTE_MODELO);

        this.repositorioJpa = Objects.requireNonNull(repositorioJpa);
    }

    @Override
    protected UnidadeFederativaJpa paraJpa(UnidadeFederativa agregado) {
        Objects.requireNonNull(agregado);

        var identificador = agregado.getIdentificador().orElseThrow().toString();
        return new UnidadeFederativaJpa(identificador, agregado.getSigla().toString(), agregado.getNome());
    }

    @Override
    protected UnidadeFederativa paraAgregado(UnidadeFederativaJpa agregadoJpa) {
        Objects.requireNonNull(agregadoJpa);

        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(agregadoJpa.getIdentificador()),
                new UnidadeFederativaSigla(agregadoJpa.getSigla()), agregadoJpa.getNome());
    }

    @Override
    protected String paraIdentificadorJpa(UnidadeFederativaCodigoIbge identificador) {
        Objects.requireNonNull(identificador);

        return identificador.toString();
    }

    @Override
    public boolean existePorSigla(UnidadeFederativaSigla sigla,
            Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
        Objects.requireNonNull(sigla);
        Objects.requireNonNull(identificadorIgnorado);

        var identificadorIgnoradoJpa = identificadorIgnorado.map(UnidadeFederativaCodigoIbge::toString).orElse(null);
        return repositorioJpa.existePorSigla(sigla.toString(), identificadorIgnoradoJpa);
    }

    @Override
    public boolean existePorNome(String nome, Optional<UnidadeFederativaCodigoIbge> identificadorIgnorado) {
        Objects.requireNonNull(nome);
        Objects.requireNonNull(identificadorIgnorado);

        var identificadorIgnoradoJpa = identificadorIgnorado.map(UnidadeFederativaCodigoIbge::toString).orElse(null);
        return repositorioJpa.existePorNome(nome, identificadorIgnoradoJpa);
    }

    @Override
    public boolean existeReferencia(UnidadeFederativaCodigoIbge identificador) {
        Objects.requireNonNull(identificador);

        return repositorioJpa.existeReferencia(identificador.toString());
    }
}

Testes do repositório

Escreva os testes do repositório de município, cobrindo, além das operações do repositório, as variações de existePorNome: na mesma unidade federativa, em outra unidade federativa, ignorando o identificador do próprio município e ignorando maiúsculas e minúsculas. Por causa da chave estrangeira, os testes persistem a unidade federativa antes dos municípios, diretamente através de IUnidadeFederativaRepositorioJpa.

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.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;

    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()));
    }
}

Pelo mesmo motivo, e porque a junção da pesquisa é interna, os testes do repositório de aplicação também persistem as unidades federativas antes dos municípios. Além dos filtros, eles cobrem a ordenação pelo nome da unidade federativa.

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/geografia/municipio/MunicipioAplicacaoRepositorioTest.java
package com.example.exemplo.infraestrutura.geografia.municipio;

import static org.junit.jupiter.api.Assertions.assertEquals;
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.aplicacao.geografia.municipio.MunicipioConsulta;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioOrdenacao;
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.geografia.unidadefederativa.IUnidadeFederativaRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.unidadefederativa.UnidadeFederativaJpa;

@DataJpaTest
class MunicipioAplicacaoRepositorioTest {
    private static final UnidadeFederativaCodigoIbge RIO_DE_JANEIRO = new UnidadeFederativaCodigoIbge("33");

    @Autowired
    private IMunicipioRepositorioJpa repositorioJpa;

    @Autowired
    private IUnidadeFederativaRepositorioJpa unidadeFederativaRepositorioJpa;

    private MunicipioRepositorio repositorio;

    private MunicipioAplicacaoRepositorio repositorioAplicacao;

    @BeforeEach
    void configurar() {
        repositorio = new MunicipioRepositorio(repositorioJpa, unidadeFederativaRepositorioJpa);
        repositorioAplicacao = new MunicipioAplicacaoRepositorio(repositorioJpa);

        unidadeFederativaRepositorioJpa.save(new UnidadeFederativaJpa("35", "SP", "São Paulo"));
        unidadeFederativaRepositorioJpa.save(new UnidadeFederativaJpa("33", "RJ", "Rio de Janeiro"));
    }

    private static Municipio criar(String codigoIbge, String nome) {
        return new Municipio(new MunicipioCodigoIbge(codigoIbge), nome, Optional.empty());
    }

    private static MunicipioConsulta consultar(Optional<String> nome, Optional<String> unidadeFederativa) {
        return consultar(MunicipioOrdenacao.NOME, nome, unidadeFederativa);
    }

    private static MunicipioConsulta consultar(MunicipioOrdenacao ordenacao, Optional<String> nome,
            Optional<String> unidadeFederativa) {
        return new MunicipioConsulta(Optional.of(ordenacao), Optional.empty(), Optional.empty(), Optional.empty(), nome,
                unidadeFederativa);
    }

    @Test
    void pesquisaRetornaProjecaoOrdenadaPorNome() {
        repositorio.incluir(criar("3550308", "São Paulo"));
        repositorio.incluir(criar("3304557", "Rio de Janeiro").extinguir());

        var resultado = repositorioAplicacao.pesquisar(consultar(Optional.empty(), Optional.empty()));

        assertEquals(2, resultado.getTotal());

        var primeira = resultado.getProjecoes().get(0);
        assertEquals(new MunicipioCodigoIbge("3304557"), primeira.getIdentificador());
        assertEquals("Rio de Janeiro", primeira.getNome());
        assertEquals(RIO_DE_JANEIRO, primeira.getUnidadeFederativa());
        assertEquals("Rio de Janeiro", primeira.getUnidadeFederativaNome());
        assertTrue(primeira.isExtinto());

        assertEquals("São Paulo", resultado.getProjecoes().get(1).getNome());
    }

    @Test
    void pesquisaOrdenadaPorNomeDaUnidadeFederativa() {
        repositorio.incluir(criar("3509502", "Campinas"));
        repositorio.incluir(criar("3303302", "Niterói"));

        var resultado = repositorioAplicacao
                .pesquisar(consultar(MunicipioOrdenacao.UNIDADE_FEDERATIVA_NOME, Optional.empty(), Optional.empty()));

        assertEquals("Niterói", resultado.getProjecoes().get(0).getNome());
        assertEquals("Campinas", resultado.getProjecoes().get(1).getNome());
    }

    @Test
    void pesquisaFiltrandoPorTrechoDoNome() {
        repositorio.incluir(criar("3550308", "São Paulo"));
        repositorio.incluir(criar("3304557", "Rio de Janeiro"));

        var resultado = repositorioAplicacao.pesquisar(consultar(Optional.of("paulo"), Optional.empty()));

        assertEquals(1, resultado.getTotal());
        assertEquals("São Paulo", resultado.getProjecoes().get(0).getNome());
    }

    @Test
    void pesquisaFiltrandoPorUnidadeFederativa() {
        repositorio.incluir(criar("3550308", "São Paulo"));
        repositorio.incluir(criar("3304557", "Rio de Janeiro"));

        var resultado = repositorioAplicacao.pesquisar(consultar(Optional.empty(), Optional.of("33")));

        assertEquals(1, resultado.getTotal());
        assertEquals("Rio de Janeiro", resultado.getProjecoes().get(0).getNome());
    }
}

Acrescente aos testes do repositório de unidade federativa a verificação de existeReferencia, persistindo um município diretamente através de IMunicipioRepositorioJpa, com a unidade federativa obtida por getReferenceById:

exemplo/infraestrutura/src/test/java/com/example/exemplo/infraestrutura/geografia/unidadefederativa/UnidadeFederativaRepositorioTest.java
package com.example.exemplo.infraestrutura.geografia.unidadefederativa;

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.unidadefederativa.UnidadeFederativa;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaCodigoIbge;
import com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaSigla;
import com.example.exemplo.infraestrutura.geografia.municipio.IMunicipioRepositorioJpa;
import com.example.exemplo.infraestrutura.geografia.municipio.MunicipioJpa;

import dev.sauloaraujo.d5.framework.dominio.AgregadoInexistenteExcecao;

@DataJpaTest
class UnidadeFederativaRepositorioTest {
    @Autowired
    private IUnidadeFederativaRepositorioJpa repositorioJpa;

    @Autowired
    private IMunicipioRepositorioJpa municipioRepositorioJpa;

    private UnidadeFederativaRepositorio repositorio;

    @BeforeEach
    void configurar() {
        repositorio = new UnidadeFederativaRepositorio(repositorioJpa);
    }

    private static UnidadeFederativa criar(String codigoIbge, String sigla, String nome) {
        return new UnidadeFederativa(new UnidadeFederativaCodigoIbge(codigoIbge), new UnidadeFederativaSigla(sigla),
                nome);
    }

    @Test
    void incluiEBuscaPorIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        var unidadeFederativa = repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35"));

        assertEquals(new UnidadeFederativaSigla("SP"), unidadeFederativa.getSigla());
        assertEquals("São Paulo", unidadeFederativa.getNome());
    }

    @Test
    void buscaPorIdentificadorInexistenteLancaExcecao() {
        assertThrows(AgregadoInexistenteExcecao.class,
                () -> repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35")));
    }

    @Test
    void existePorIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("35")));
        assertFalse(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("33")));
    }

    @Test
    void altera() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        repositorio.alterar(criar("35", "SP", "São Paulo Alterado"));

        var unidadeFederativa = repositorio.buscarPorIdentificador(new UnidadeFederativaCodigoIbge("35"));
        assertEquals("São Paulo Alterado", unidadeFederativa.getNome());
    }

    @Test
    void exclui() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        repositorio.excluir(new UnidadeFederativaCodigoIbge("35"));

        assertFalse(repositorio.existePorIdentificador(new UnidadeFederativaCodigoIbge("35")));
    }

    @Test
    void existePorSiglaConsiderandoTodasAsUnidadesFederativas() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), Optional.empty()));
        assertFalse(repositorio.existePorSigla(new UnidadeFederativaSigla("RJ"), Optional.empty()));
    }

    @Test
    void existePorSiglaIgnorandoOProprioIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("35"));

        assertFalse(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), identificador));
    }

    @Test
    void existePorSiglaNaoIgnorandoUmaUnidadeFederativaDiferente() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("33"));

        assertTrue(repositorio.existePorSigla(new UnidadeFederativaSigla("SP"), identificador));
    }

    @Test
    void existePorNomeConsiderandoTodasAsUnidadesFederativas() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorNome("São Paulo", Optional.empty()));
        assertFalse(repositorio.existePorNome("Rio de Janeiro", Optional.empty()));
    }

    @Test
    void existePorNomeIgnorandoOProprioIdentificador() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("35"));

        assertFalse(repositorio.existePorNome("São Paulo", identificador));
    }

    @Test
    void existePorNomeNaoIgnorandoUmaUnidadeFederativaDiferente() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));

        var identificador = Optional.of(new UnidadeFederativaCodigoIbge("33"));

        assertTrue(repositorio.existePorNome("São Paulo", identificador));
    }

    @Test
    void existePorNomeIgnorandoMaiusculasEMinusculas() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));

        assertTrue(repositorio.existePorNome("SÃO PAULO", Optional.empty()));
    }

    @Test
    void existeReferenciaPorMunicipio() {
        repositorio.incluir(criar("35", "SP", "São Paulo"));
        repositorio.incluir(criar("33", "RJ", "Rio de Janeiro"));
        municipioRepositorioJpa
                .save(new MunicipioJpa("3550308", "São Paulo", repositorioJpa.getReferenceById("35"), false));

        assertTrue(repositorio.existeReferencia(new UnidadeFederativaCodigoIbge("35")));
        assertFalse(repositorio.existeReferencia(new UnidadeFederativaCodigoIbge("33")));
    }
}

Execute os testes da infraestrutura:

mvn -pl infraestrutura -am test

UI

O município é exposto no pacote com.example.exemplo.ui.backend.geografia.municipio. Crie o package-info.java desse pacote, anotado com @NullMarked:

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/geografia/municipio/package-info.java
@org.jspecify.annotations.NullMarked
package com.example.exemplo.ui.backend.geografia.municipio;

Controlador

Além das operações herdadas de AgregadoControladorAbstrato, o controlador expõe a ação de extinguir em POST {identificador}/extinguir, delegando a MunicipioServico.extinguir. Como todo método mapeado para POST, ela é executada em uma transação graças ao gerenciamento de transações.

exemplo/ui-backend/src/main/java/com/example/exemplo/ui/backend/geografia/municipio/MunicipioControlador.java
package com.example.exemplo.ui.backend.geografia.municipio;

import java.util.Objects;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.exemplo.aplicacao.geografia.municipio.MunicipioAplicacaoServico;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioConsulta;
import com.example.exemplo.aplicacao.geografia.municipio.MunicipioProjecao;
import com.example.exemplo.dominio.geografia.municipio.Municipio;
import com.example.exemplo.dominio.geografia.municipio.MunicipioAutorizacoes;
import com.example.exemplo.dominio.geografia.municipio.MunicipioCodigoIbge;
import com.example.exemplo.dominio.geografia.municipio.MunicipioServico;

import dev.sauloaraujo.d5.framework.ui.AgregadoControladorAbstrato;
import dev.sauloaraujo.d5.framework.ui.Backend;

@RestController
@RequestMapping(Backend.PREFIXO + "geografia/municipio")
public class MunicipioControlador extends
        AgregadoControladorAbstrato<MunicipioCodigoIbge, Municipio, MunicipioAutorizacoes, MunicipioProjecao, MunicipioConsulta> {
    private final MunicipioServico servico;

    public MunicipioControlador(MunicipioServico servico, MunicipioAplicacaoServico servicoAplicacao) {
        super(servico, servicoAplicacao);

        this.servico = Objects.requireNonNull(servico);
    }

    @PostMapping("{identificador}/extinguir")
    public void extinguir(@PathVariable MunicipioCodigoIbge identificador) {
        Objects.requireNonNull(identificador);

        servico.extinguir(identificador);
    }
}

Autenticação

O município não precisa entrar no recurso de autenticação: não altere AutenticacaoRecurso nem AutenticacaoControlador. O recurso continua trazendo apenas o usuário e as autorizações sobre a unidade federativa.

Testando com o Swagger UI

Encerre a aplicação, se ela ainda estiver em execução, e suba novamente BackendAplicacao — pela IDE ou executando de novo, no diretório exemplo, os dois comandos da lição 1: sem o mvn install, o spring-boot:run usaria os módulos instalados no repositório Maven local ao final da lição 1, que não têm o município. Em seguida, acesse o Swagger UI e autentique-se como na lição 1. As operações de município aparecem sob a tag municipio-controlador, inclusive a de extinguir:

Operações de MunicipioControlador no Swagger UI, incluindo extinguir

Como o banco de dados em memória é recriado a cada inicialização, inclua de novo a unidade federativa de São Paulo, como na lição 1 — sem ela, a inclusão do município responde 404. Em seguida, inclua o município de São Paulo através de POST /backend/geografia/municipio/incluir. O corpo de exemplo sugerido pelo Swagger UI traz, além de identificador e nome, os atributos extinto, com o valor true, e unidadeFederativa: remova os dois. Com extinto igual a true, a inclusão responde 422, porque um município só se torna extinto pela ação de extinguir; omitido, o indicador assume false. Já unidadeFederativa aparece no exemplo porque o município a expõe através de getUnidadeFederativa, mas é derivada do identificador e, por isso, ignorada na requisição:

{
  "identificador": "3550308",
  "nome": "São Paulo"
}

Corpo da requisição de inclusão preenchido com o município de São Paulo

POST /backend/geografia/municipio/{identificador}/extinguir recebe apenas o identificador no caminho e extingue o município:

Parâmetro identificador preenchido com 3550308 na operação de extinguir

Depois disso, a alteração do município responde 409, com a mensagem O município 3550308 está extinto. O recurso devolvido por GET /backend/geografia/municipio/{identificador}/alteracao mostra o município extinto e inclui a autorização extinguir, que o cliente pode usar para exibir ou ocultar a ação. Como as autorizações não dependem do estado do município, extinguir continua true para um administrador; para saber se a ação ainda é possível, o cliente consulta o atributo extinto do município:

Resposta 200 da alteração, com o município extinto e a autorização extinguir

Por fim, tente excluir a unidade federativa de São Paulo: como ela é referenciada pelo município, a exclusão responde 409, com a mensagem A unidade federativa 35 é referenciada e não pode ser excluída.

Resposta 409 da exclusão da unidade federativa referenciada pelo município

Na lição 3, você vai criar o subdomínio CRM e o cliente, que referencia o município.