From ce4c011890af8d6eb83eff2af627d265b4f60a4f Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sun, 4 Oct 2026 11:08:35 -0300 Subject: [PATCH 1/9] feat(onboarding): adiciona DTOs e mapper do onboarding --- .../dto/core/empresa/OnboardingDadosDTO.java | 13 ++ .../empresa/OnboardingInicioRequestDTO.java | 23 +++ .../dto/core/empresa/OnboardingMapper.java | 36 +++++ .../core/empresa/OnboardingRequestDTO.java | 145 ++++++++++++++++++ .../core/empresa/OnboardingResponseDTO.java | 16 ++ 5 files changed, 233 insertions(+) create mode 100644 src/main/java/br/com/acta/dto/core/empresa/OnboardingDadosDTO.java create mode 100644 src/main/java/br/com/acta/dto/core/empresa/OnboardingInicioRequestDTO.java create mode 100644 src/main/java/br/com/acta/dto/core/empresa/OnboardingMapper.java create mode 100644 src/main/java/br/com/acta/dto/core/empresa/OnboardingRequestDTO.java create mode 100644 src/main/java/br/com/acta/dto/core/empresa/OnboardingResponseDTO.java diff --git a/src/main/java/br/com/acta/dto/core/empresa/OnboardingDadosDTO.java b/src/main/java/br/com/acta/dto/core/empresa/OnboardingDadosDTO.java new file mode 100644 index 0000000..c61ef88 --- /dev/null +++ b/src/main/java/br/com/acta/dto/core/empresa/OnboardingDadosDTO.java @@ -0,0 +1,13 @@ +package br.com.acta.dto.core.empresa; + +import br.com.acta.entity.enums.StatusGeral; +import br.com.acta.entity.enums.TipoUsuario; + +record OnboardingDadosDTO( + Long idEmpresa, + StatusGeral statusEmpresa, + Long idUsuario, + TipoUsuario tipoUsuario, + StatusGeral statusUsuario +) { +} diff --git a/src/main/java/br/com/acta/dto/core/empresa/OnboardingInicioRequestDTO.java b/src/main/java/br/com/acta/dto/core/empresa/OnboardingInicioRequestDTO.java new file mode 100644 index 0000000..47f2ecc --- /dev/null +++ b/src/main/java/br/com/acta/dto/core/empresa/OnboardingInicioRequestDTO.java @@ -0,0 +1,23 @@ +package br.com.acta.dto.core.empresa; + +import br.com.acta.common.config.swagger.examples.SwaggerRequestExamples; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.br.CNPJ; + +public record OnboardingInicioRequestDTO( + @Schema(description = "Dados do primeiro gestor da empresa") + @NotNull(message = "{validation.onboarding.gestor.notnull}") + @Valid + OnboardingRequestDTO.Gestor gestor, + + @Schema(description = "CNPJ da empresa que o gestor deseja acessar", example = SwaggerRequestExamples.CNPJ) + @NotBlank(message = "{validation.empresa.cnpj.notblank}") + @Size(min = 14, max = 14, message = "{validation.empresa.cnpj.size}") + @CNPJ(message = "{validation.empresa.cnpj.invalid}") + String cnpj +) { +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/dto/core/empresa/OnboardingMapper.java b/src/main/java/br/com/acta/dto/core/empresa/OnboardingMapper.java new file mode 100644 index 0000000..cf9b9df --- /dev/null +++ b/src/main/java/br/com/acta/dto/core/empresa/OnboardingMapper.java @@ -0,0 +1,36 @@ +package br.com.acta.dto.core.empresa; + +import br.com.acta.entity.core.Usuario; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(componentModel = "spring") +public interface OnboardingMapper { + @Mapping(source = "empresa.id", target = "idEmpresa") + @Mapping(source = "empresa.status", target = "statusEmpresa") + @Mapping(source = "id", target = "idUsuario") + @Mapping(source = "tipo", target = "tipoUsuario") + @Mapping(source = "status", target = "statusUsuario") + OnboardingDadosDTO toDados(Usuario usuario); + + default OnboardingResponseDTO toResponse(Usuario usuario) { + return toResponse(usuario, false, false, false); + } + + default OnboardingResponseDTO toResponse(Usuario usuario, boolean empresaExistente, boolean conviteEnviado, boolean proximaEtapa) { + OnboardingDadosDTO dados = toDados(usuario); + return new OnboardingResponseDTO( + empresaExistente, + conviteEnviado, + proximaEtapa, + dados.idEmpresa(), + dados.statusEmpresa(), + dados.idUsuario(), + dados.tipoUsuario(), + dados.statusUsuario()); + } + + default OnboardingResponseDTO solicitarDadosEmpresa() { + return new OnboardingResponseDTO(false, false, true, null, null, null, null, null); + } +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/dto/core/empresa/OnboardingRequestDTO.java b/src/main/java/br/com/acta/dto/core/empresa/OnboardingRequestDTO.java new file mode 100644 index 0000000..4161191 --- /dev/null +++ b/src/main/java/br/com/acta/dto/core/empresa/OnboardingRequestDTO.java @@ -0,0 +1,145 @@ +package br.com.acta.dto.core.empresa; + +import br.com.acta.common.config.swagger.examples.SwaggerRequestExamples; +import br.com.acta.entity.enums.TamanhoEmpresa; +import br.com.acta.entity.enums.UF; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import org.hibernate.validator.constraints.br.CPF; +import org.hibernate.validator.constraints.br.CNPJ; + +import java.time.LocalDate; + +public record OnboardingRequestDTO( + @Schema(description = "Dados do primeiro gestor da empresa") + @NotNull(message = "{validation.onboarding.gestor.notnull}") + @Valid + Gestor gestor, + + @Schema(description = "Dados da empresa") + @NotNull(message = "{validation.onboarding.empresa.notnull}") + @Valid + Empresa empresa, + + @Schema(description = "Endereço principal da empresa") + @NotNull(message = "{validation.onboarding.endereco.notnull}") + @Valid + Endereco endereco +) { + public record Gestor( + @Schema(description = "CPF do gestor", example = SwaggerRequestExamples.CPF) + @CPF(message = "{validation.colaborador.cpf.invalid}") + @NotBlank(message = "{validation.colaborador.cpf.notblank}") + @Size(min = 11, max = 11, message = "{validation.colaborador.cpf.size}") + String cpf, + + @Schema(description = "Nome do gestor", example = SwaggerRequestExamples.NOME) + @NotBlank(message = "{validation.colaborador.nome.notblank}") + @Size(max = 160, message = "{validation.nome.size}") + String nome, + + @Schema(description = "Cargo do gestor", example = SwaggerRequestExamples.CARGO) + @NotBlank(message = "{validation.colaborador.cargo.notblank}") + @Size(max = 100, message = "{validation.colaborador.cargo.size}") + String cargo, + + @Schema(description = "Área do gestor", example = SwaggerRequestExamples.AREA) + @NotBlank(message = "{validation.area.notblank}") + @Size(max = 100, message = "{validation.area.size}") + String area, + + @Schema(description = "Data de nascimento do gestor", example = SwaggerRequestExamples.DATA_NASCIMENTO, format = "yyyy-MM-dd", type = "string") + @JsonFormat(pattern = "yyyy-MM-dd") + @NotNull(message = "{validation.colaborador.dataNascimento.notnull}") + @Past(message = "{validation.colaborador.dataNascimento.past}") + LocalDate dataNascimento, + + @Schema(description = "Data de contratação do gestor", example = SwaggerRequestExamples.DATA_CONTRATACAO, format = "yyyy-MM-dd", type = "string") + @JsonFormat(pattern = "yyyy-MM-dd") + @NotNull(message = "{validation.colaborador.dataContratacao.notnull}") + @PastOrPresent(message = "{validation.colaborador.dataContratacao.pastorpresent}") + LocalDate dataContratacao, + + @Schema(description = "E-mail de acesso do gestor", example = SwaggerRequestExamples.EMAIL) + @NotBlank(message = "{validation.email.notblank}") + @Email(message = "{validation.email.invalid}") + @Size(max = 254, message = "{validation.email.size}") + String email + ) {} + + public record Empresa( + @Schema(description = "CNPJ da empresa", example = SwaggerRequestExamples.CNPJ) + @NotBlank(message = "{validation.empresa.cnpj.notblank}") + @Size(min = 14, max = 14, message = "{validation.empresa.cnpj.size}") + @CNPJ(message = "{validation.empresa.cnpj.invalid}") + String cnpj, + + @Schema(description = "Nome da empresa", example = SwaggerRequestExamples.EMPRESA_NOME) + @NotBlank(message = "{validation.empresa.nome.notblank}") + @Size(max = 160, message = "{validation.empresa.nome.size}") + String nome, + + @Schema(description = "Tamanho da empresa", example = SwaggerRequestExamples.TAMANHO_EMPRESA, implementation = TamanhoEmpresa.class) + @NotNull(message = "{validation.empresa.tamanho.notnull}") + TamanhoEmpresa tamanhoEmpresa, + + @Schema(description = "Setor da empresa", example = SwaggerRequestExamples.SETOR) + @NotBlank(message = "{validation.empresa.setor.notblank}") + @Size(max = 100, message = "{validation.empresa.setor.size}") + String setorEmpresa, + + @Schema(description = "E-mail principal da empresa", example = SwaggerRequestExamples.EMAIL) + @NotBlank(message = "{validation.email.notblank}") + @Email(message = "{validation.email.invalid}") + @Size(max = 254, message = "{validation.email.size}") + String emailEmpresa, + + @Schema(description = "Telefone principal da empresa", example = SwaggerRequestExamples.TELEFONE) + @NotBlank(message = "{validation.telefone.numero.notblank}") + @Pattern(regexp = "^\\d{10,11}$", message = "{validation.telefone.numero.pattern}") + String telefoneEmpresa + ) {} + + public record Endereco( + @Schema(description = "CEP do endereço", example = SwaggerRequestExamples.CEP) + @NotBlank(message = "{validation.cep.notblank}") + @Pattern(regexp = "\\d{8}", message = "{validation.cep.pattern}") + String cep, + + @Schema(description = "UF do endereço", example = SwaggerRequestExamples.UF, implementation = UF.class) + @NotNull(message = "{validation.uf.notnull}") + UF uf, + + @Schema(description = "Cidade do endereço", example = SwaggerRequestExamples.CIDADE) + @NotBlank(message = "{validation.cidade.notblank}") + @Size(max = 100, message = "{validation.cidade.size}") + String cidade, + + @Schema(description = "Bairro do endereço", example = SwaggerRequestExamples.BAIRRO) + @NotBlank(message = "{validation.bairro.notblank}") + @Size(max = 100, message = "{validation.bairro.size}") + String bairro, + + @Schema(description = "Logradouro do endereço", example = SwaggerRequestExamples.LOGRADOURO) + @NotBlank(message = "{validation.logradouro.notblank}") + @Size(max = 180, message = "{validation.logradouro.size}") + String logradouro, + + @Schema(description = "Número do endereço", example = SwaggerRequestExamples.NUMERO_ENDERECO) + @NotBlank(message = "{validation.numero.notblank}") + @Size(max = 20, message = "{validation.numero.size}") + String numeroEndereco, + + @Schema(description = "Complemento do endereço", example = SwaggerRequestExamples.COMPLEMENTO) + @Size(max = 1000, message = "{validation.complemento.size}") + String complemento + ) {} +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/dto/core/empresa/OnboardingResponseDTO.java b/src/main/java/br/com/acta/dto/core/empresa/OnboardingResponseDTO.java new file mode 100644 index 0000000..bee2d60 --- /dev/null +++ b/src/main/java/br/com/acta/dto/core/empresa/OnboardingResponseDTO.java @@ -0,0 +1,16 @@ +package br.com.acta.dto.core.empresa; + +import br.com.acta.entity.enums.StatusGeral; +import br.com.acta.entity.enums.TipoUsuario; + +public record OnboardingResponseDTO( + Boolean empresaExistente, + Boolean conviteEnviado, + Boolean proximaEtapa, + Long idEmpresa, + StatusGeral statusEmpresa, + Long idUsuario, + TipoUsuario tipoUsuario, + StatusGeral statusUsuario +) { +} \ No newline at end of file From 3093bc498cd8a39df3b15f3e8e04c9d5bc178f0c Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sun, 4 Oct 2026 11:11:02 -0300 Subject: [PATCH 2/9] =?UTF-8?q?refactor/openapi:=20reorganiza=20imports=20?= =?UTF-8?q?e=20adiciona=20m=C3=A9todo=20de=20busca=20por=20CNPJ=20no=20rep?= =?UTF-8?q?osit=C3=B3rio=20de=20empresas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acta/common/config/swagger/openapi/EmpresaOpenapi.java | 5 ++--- .../br/com/acta/repository/padrao/EmpresaRepository.java | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/br/com/acta/common/config/swagger/openapi/EmpresaOpenapi.java b/src/main/java/br/com/acta/common/config/swagger/openapi/EmpresaOpenapi.java index 9bebd07..99a36ce 100644 --- a/src/main/java/br/com/acta/common/config/swagger/openapi/EmpresaOpenapi.java +++ b/src/main/java/br/com/acta/common/config/swagger/openapi/EmpresaOpenapi.java @@ -1,10 +1,10 @@ package br.com.acta.common.config.swagger.openapi; import br.com.acta.common.config.swagger.annotation.ApiAuthenticationResponses; -import br.com.acta.common.config.swagger.annotation.ApiResourceResponses; import br.com.acta.common.config.swagger.annotation.ApiBadRequestResponse; import br.com.acta.common.config.swagger.annotation.ApiConflictResponse; import br.com.acta.common.config.swagger.annotation.ApiNotFoundResponse; +import br.com.acta.common.config.swagger.annotation.ApiResourceResponses; import br.com.acta.common.config.swagger.annotation.ApiUnsupportedMediaTypeResponse; import br.com.acta.common.config.swagger.examples.SwaggerOpenapiDescriptions; import br.com.acta.common.config.swagger.examples.SwaggerParameterDescriptions; @@ -16,9 +16,9 @@ import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.annotations.parameters.RequestBody; import org.springframework.http.ResponseEntity; import java.util.List; @@ -26,7 +26,6 @@ @Tag(name = "Empresas", description = SwaggerOpenapiDescriptions.EMPRESA_CONTROLLER) public interface EmpresaOpenapi { - @Operation(summary = "Lista as empresas") @ApiResponse(responseCode = "200", description = "Empresas encontradas", content = @Content(array = @ArraySchema(schema = @Schema(implementation = EmpresaResponseDTO.class)))) @ApiAuthenticationResponses diff --git a/src/main/java/br/com/acta/repository/padrao/EmpresaRepository.java b/src/main/java/br/com/acta/repository/padrao/EmpresaRepository.java index f351b99..6cb5ab0 100644 --- a/src/main/java/br/com/acta/repository/padrao/EmpresaRepository.java +++ b/src/main/java/br/com/acta/repository/padrao/EmpresaRepository.java @@ -10,7 +10,8 @@ public interface EmpresaRepository extends BaseRepository { Optional findByIdAndId(Long id, Long idEmpresa); + Optional findByCnpj(String cnpj); boolean existsByCnpj(String cnpj); List findAllByStatus(StatusGeral status); List findByTamanhoAndStatus(TamanhoEmpresa tamanho, StatusGeral status); -} +} \ No newline at end of file From 73d1f1ef3eda1694f5941edaef012c9f8c406304 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sun, 4 Oct 2026 11:12:43 -0300 Subject: [PATCH 3/9] feat/onboarding: implementa interface e controlador para onboarding de empresas e gestores --- .../swagger/openapi/OnboardingOpenapi.java | 34 ++++ .../acta/controller/OnboardingController.java | 33 ++++ .../com/acta/service/OnboardingService.java | 171 ++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java create mode 100644 src/main/java/br/com/acta/controller/OnboardingController.java create mode 100644 src/main/java/br/com/acta/service/OnboardingService.java diff --git a/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java b/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java new file mode 100644 index 0000000..89ba9d0 --- /dev/null +++ b/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java @@ -0,0 +1,34 @@ +package br.com.acta.common.config.swagger.openapi; + +import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; +import br.com.acta.common.config.swagger.annotation.ApiAuthenticationResponses; +import br.com.acta.common.config.swagger.annotation.ApiBadRequestResponse; +import br.com.acta.common.config.swagger.annotation.ApiConflictResponse; +import br.com.acta.dto.core.empresa.OnboardingInicioRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingResponseDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; + +@Tag(name = "Onboarding", description = "Fluxo de entrada de administradores e criação de empresas") +public interface OnboardingOpenapi { + @Operation(summary = "Verifica a empresa e inicia o onboarding do gestor") + @ApiResponse(responseCode = "200", description = "Empresa identificada ou próxima etapa liberada", content = @Content(schema = @Schema(implementation = OnboardingResponseDTO.class))) + @ApiAuthenticationResponses + @ApiBadRequestResponse + @ApiConflictResponse + ResponseEntity iniciar(@Parameter(hidden = true) FirebaseIdentity identity, @RequestBody(description = "Dados do gestor e CNPJ da empresa", required = true, content = @Content(schema = @Schema(implementation = OnboardingInicioRequestDTO.class))) OnboardingInicioRequestDTO dto); + + @Operation(summary = "Cadastra a empresa e seu primeiro gestor") + @ApiResponse(responseCode = "201", description = "Cadastro criado aguardando liberação", content = @Content(schema = @Schema(implementation = OnboardingResponseDTO.class))) + @ApiAuthenticationResponses + @ApiBadRequestResponse + @ApiConflictResponse + ResponseEntity cadastrarEmpresa(@Parameter(hidden = true) FirebaseIdentity identity, @RequestBody(description = "Dados do gestor, empresa e endereço principal", required = true, content = @Content(schema = @Schema(implementation = OnboardingRequestDTO.class))) OnboardingRequestDTO dto); +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/controller/OnboardingController.java b/src/main/java/br/com/acta/controller/OnboardingController.java new file mode 100644 index 0000000..6741438 --- /dev/null +++ b/src/main/java/br/com/acta/controller/OnboardingController.java @@ -0,0 +1,33 @@ +package br.com.acta.controller; + +import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; +import br.com.acta.common.config.swagger.openapi.OnboardingOpenapi; +import br.com.acta.dto.core.empresa.OnboardingInicioRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingResponseDTO; +import br.com.acta.service.OnboardingService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/api/v1/onboarding", produces = MediaType.APPLICATION_JSON_VALUE) +@RequiredArgsConstructor +public class OnboardingController implements OnboardingOpenapi { + private final OnboardingService service; + + @PostMapping("/inicio") + @Override + public ResponseEntity iniciar(@AuthenticationPrincipal FirebaseIdentity identity, @RequestBody @Valid OnboardingInicioRequestDTO dto) { + return ResponseEntity.ok(service.iniciar(identity, dto)); + } + + @PostMapping("/gestor") + @Override + public ResponseEntity cadastrarEmpresa(@AuthenticationPrincipal FirebaseIdentity identity, @RequestBody @Valid OnboardingRequestDTO dto) { + return ResponseEntity.status(201).body(service.cadastrarEmpresa(identity, dto)); + } +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/service/OnboardingService.java b/src/main/java/br/com/acta/service/OnboardingService.java new file mode 100644 index 0000000..2351e64 --- /dev/null +++ b/src/main/java/br/com/acta/service/OnboardingService.java @@ -0,0 +1,171 @@ +package br.com.acta.service; + +import br.com.acta.common.client.BrevoClient; +import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; +import br.com.acta.common.handler.exception.BusinessRuleException; +import br.com.acta.common.handler.exception.FirebaseAccessRevokedException; +import br.com.acta.common.handler.exception.RegexException; +import br.com.acta.common.handler.exception.UniqueViolationException; +import br.com.acta.common.utils.TokenUtils; +import br.com.acta.dto.core.colaborador.ColaboradorMapper; +import br.com.acta.dto.core.colaborador.ColaboradorRequestDTO; +import br.com.acta.dto.core.contato.email.EmailRequestDTO; +import br.com.acta.dto.core.contato.telefone.TelefoneRequestDTO; +import br.com.acta.dto.core.empresa.EmpresaMapper; +import br.com.acta.dto.core.empresa.EmpresaRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingInicioRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingMapper; +import br.com.acta.dto.core.empresa.OnboardingRequestDTO; +import br.com.acta.dto.core.empresa.OnboardingResponseDTO; +import br.com.acta.dto.core.empresa.endereco.EnderecoRequestDTO; +import br.com.acta.dto.core.usuario.UsuarioMapper; +import br.com.acta.dto.core.usuario.UsuarioRequestDTO; +import br.com.acta.entity.core.Colaborador; +import br.com.acta.entity.core.Convite; +import br.com.acta.entity.core.Empresa; +import br.com.acta.entity.core.Usuario; +import br.com.acta.entity.enums.StatusGeral; +import br.com.acta.entity.enums.TipoUsuario; +import br.com.acta.repository.padrao.ColaboradorRepository; +import br.com.acta.repository.padrao.ConviteRepository; +import br.com.acta.repository.padrao.EmpresaRepository; +import br.com.acta.repository.padrao.UsuarioRepository; +import br.com.caelum.stella.validation.CNPJValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.OffsetDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class OnboardingService { + private static final int VALIDADE_CONVITE_MINUTOS = 30; + + private final EmpresaRepository empresaRepo; + private final UsuarioRepository usuarioRepo; + private final ColaboradorRepository colaboradorRepo; + private final ConviteRepository conviteRepo; + private final EmpresaMapper empresaMapper; + private final ColaboradorMapper colaboradorMapper; + private final UsuarioMapper usuarioMapper; + private final OnboardingMapper onboardingMapper; + private final BrevoClient brevoClient; + private final CNPJValidator cnpjValidator = new CNPJValidator(); + + @PreAuthorize("hasAuthority('ROLE_FIREBASE')") + @Transactional + public OnboardingResponseDTO iniciar(FirebaseIdentity identity, OnboardingInicioRequestDTO dto) { + validarIdentidade(identity, dto.gestor().email()); + + if (usuarioRepo.existsByEmailLoginIgnoreCase(identity.email())) throw new UniqueViolationException("E-mail"); + if (colaboradorRepo.existsByCpf(dto.gestor().cpf())) throw new UniqueViolationException("CPF"); + if (!cnpjValidator.isEligible(dto.cnpj())) throw new RegexException("CNPJ"); + + Empresa empresa = empresaRepo.findByCnpj(dto.cnpj()).orElse(null); + if (empresa == null) return onboardingMapper.solicitarDadosEmpresa(); + + Usuario criadoPor = usuarioRepo.findByTipoAndStatusAndEmpresaId(TipoUsuario.ADMIN, StatusGeral.ATIVO, empresa.getId()) + .stream() + .findFirst() + .orElseThrow(() -> new BusinessRuleException("A empresa não possui um administrador ativo")); + + Usuario usuario = criarUsuarioPendente(dto.gestor(), identity.email(), empresa); + Colaborador colaborador = criarColaboradorPendente(dto.gestor(), identity.email(), empresa, usuario); + usuario.setColaborador(colaborador); + + String codigo = TokenUtils.gerarCodigo(); + Convite convite = new Convite(); + convite.setUsuario(usuario); + convite.setEmailDestino(usuario.getEmailLogin()); + convite.setTokenHash(TokenUtils.hashToken(codigo)); + convite.setStatus("PENDENTE"); + convite.setExpiraEm(OffsetDateTime.now().plusMinutes(VALIDADE_CONVITE_MINUTOS)); + convite.setCriadoPor(criadoPor); + Convite conviteSalvo = conviteRepo.save(convite); + + conviteRepo.flush(); + brevoClient.enviarConviteAdmin(conviteSalvo.getEmailDestino(), usuario.getNome(), empresa.getNome(), conviteSalvo.getExpiraEm(), codigo); + + return onboardingMapper.toResponse(usuario, true, true, false); + } + + @PreAuthorize("hasAuthority('ROLE_FIREBASE')") + @Transactional + public OnboardingResponseDTO cadastrarEmpresa(FirebaseIdentity identity, OnboardingRequestDTO dto) { + validarIdentidade(identity, dto.gestor().email()); + + if (!cnpjValidator.isEligible(dto.empresa().cnpj())) throw new RegexException("CNPJ"); + if (empresaRepo.existsByCnpj(dto.empresa().cnpj())) return iniciar(identity, new OnboardingInicioRequestDTO(dto.gestor(), dto.empresa().cnpj())); + if (usuarioRepo.existsByEmailLoginIgnoreCase(identity.email())) throw new UniqueViolationException("E-mail"); + if (colaboradorRepo.existsByCpf(dto.gestor().cpf())) throw new UniqueViolationException("CPF"); + + EmpresaRequestDTO empresaRequest = new EmpresaRequestDTO( + dto.empresa().cnpj(), + dto.empresa().nome(), + dto.empresa().tamanhoEmpresa(), + dto.empresa().setorEmpresa(), + List.of(new TelefoneRequestDTO(dto.empresa().telefoneEmpresa(), true)), + List.of(new EmailRequestDTO(dto.empresa().emailEmpresa(), true)), + List.of(new EnderecoRequestDTO( + dto.endereco().cep(), + dto.endereco().uf(), + dto.endereco().cidade(), + dto.endereco().bairro(), + dto.endereco().logradouro(), + dto.endereco().numeroEndereco(), + dto.endereco().complemento(), + true))); + + Empresa empresa = empresaMapper.toEntity(empresaRequest); + empresa.setStatus(StatusGeral.PENDENTE); + Empresa empresaSalva = empresaRepo.save(empresa); + + Usuario usuario = criarUsuarioPendente(dto.gestor(), identity.email(), empresaSalva, identity.firebaseUid()); + Colaborador colaborador = criarColaboradorPendente(dto.gestor(), identity.email(), empresaSalva, usuario); + usuario.setColaborador(colaborador); + + return onboardingMapper.toResponse(usuario, false, false, false); + } + + private void validarIdentidade(FirebaseIdentity identity, String emailGestor) { + if (identity == null || !identity.emailVerificado() || identity.email() == null || !identity.email().equalsIgnoreCase(emailGestor)) + throw new FirebaseAccessRevokedException(); + } + + private Usuario criarUsuarioPendente(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa) { + return criarUsuarioPendente(gestor, email, empresa, null); + } + + private Usuario criarUsuarioPendente(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa, String firebaseUid) { + UsuarioRequestDTO request = new UsuarioRequestDTO(gestor.nome(), email.trim(), firebaseUid, TipoUsuario.ADMIN); + Usuario usuario = usuarioMapper.toEntity(request); + usuario.setStatus(StatusGeral.PENDENTE); + usuario.setEmpresa(empresa); + return usuarioRepo.save(usuario); + } + + private Colaborador criarColaboradorPendente(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa, Usuario usuario) { + UsuarioRequestDTO usuarioRequest = new UsuarioRequestDTO(gestor.nome(), email, usuario.getFirebaseUid(), TipoUsuario.ADMIN); + ColaboradorRequestDTO request = new ColaboradorRequestDTO( + gestor.cpf(), + gestor.nome(), + gestor.nome(), + gestor.cargo(), + gestor.area(), + gestor.dataNascimento(), + gestor.dataContratacao(), + true, + List.of(new EmailRequestDTO(email, true)), + List.of(), + usuarioRequest); + + Colaborador colaborador = colaboradorMapper.toEntity(request); + colaborador.setStatus(StatusGeral.PENDENTE); + colaborador.setEmpresa(empresa); + colaborador.setUsuario(usuario); + return colaboradorRepo.save(colaborador); + } +} \ No newline at end of file From 9bdd44c2198f5b73229fe8cf19c5f056ed332a91 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sun, 4 Oct 2026 11:13:36 -0300 Subject: [PATCH 4/9] =?UTF-8?q?feat/firebase:=20adiciona=20rotas=20de=20on?= =?UTF-8?q?boarding=20e=20atualiza=20filtro=20de=20autentica=C3=A7=C3=A3o?= =?UTF-8?q?=20para=20incluir=20novos=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/firebase/FirebaseAuthFilter.java | 18 +++++++++++++----- .../common/config/security/SecurityConfig.java | 5 ++++- .../examples/SwaggerOpenapiDescriptions.java | 12 +++++++++++- .../br/com/acta/controller/AuthController.java | 6 ++++-- .../java/br/com/acta/service/AuthService.java | 11 +++++++++++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/main/java/br/com/acta/common/config/firebase/FirebaseAuthFilter.java b/src/main/java/br/com/acta/common/config/firebase/FirebaseAuthFilter.java index ab19d8e..165a183 100644 --- a/src/main/java/br/com/acta/common/config/firebase/FirebaseAuthFilter.java +++ b/src/main/java/br/com/acta/common/config/firebase/FirebaseAuthFilter.java @@ -26,6 +26,9 @@ @RequiredArgsConstructor public class FirebaseAuthFilter extends OncePerRequestFilter { private static final String AUTH = "/api/v1/auth/ativar"; + private static final String ONBOARDING = "/api/v1/onboarding/inicio"; + private static final String ONBOARDING_EMPRESA = "/api/v1/onboarding/gestor"; + private static final String ME = "/api/v1/me"; private final FirebaseUtils utils; private final ObjectMapper mapper; @@ -50,7 +53,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse FirebaseToken idToken = utils.verificarIdToken(token); - if (ehRotaAutenticacao(request)) { + if (ehRotaIdentidadeFirebase(request)) { Principal principal = new FirebaseIdentity(idToken.getUid(), idToken.getEmail(), idToken.isEmailVerified()); utils.autenticar(request, principal, List.of(new SimpleGrantedAuthority("ROLE_FIREBASE"))); } else { @@ -59,7 +62,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } filterChain.doFilter(request, response); - } catch (BadCredentialsException bce){ + } catch (BadCredentialsException bce) { SecurityContextHolder.clearContext(); responderErro(response, HttpStatus.UNAUTHORIZED, "O ID Token do Firebase não existe ou está inválido"); } catch (AccessDeniedException ade) { @@ -75,11 +78,16 @@ public String getName() { } } - private boolean ehRotaAutenticacao(HttpServletRequest request) { - return request.getServletPath().equals(AUTH) && "POST".equalsIgnoreCase(request.getMethod()); + private boolean ehRotaIdentidadeFirebase(HttpServletRequest request) { + String path = request.getServletPath(); + String method = request.getMethod(); + return (path.equals(AUTH) && "POST".equalsIgnoreCase(method)) + || (path.equals(ONBOARDING) && "POST".equalsIgnoreCase(method)) + || (path.equals(ONBOARDING_EMPRESA) && "POST".equalsIgnoreCase(method)) + || (path.equals(ME) && "GET".equalsIgnoreCase(method)); } - private void responderErro(HttpServletResponse resp, HttpStatus status, String mensagem) throws IOException { + private void responderErro(HttpServletResponse resp, HttpStatus status, String mensagem) throws IOException { resp.setStatus(status.value()); resp.setContentType(MediaType.APPLICATION_JSON_VALUE); resp.setCharacterEncoding(StandardCharsets.UTF_8.name()); diff --git a/src/main/java/br/com/acta/common/config/security/SecurityConfig.java b/src/main/java/br/com/acta/common/config/security/SecurityConfig.java index 563683b..9c5c1c3 100644 --- a/src/main/java/br/com/acta/common/config/security/SecurityConfig.java +++ b/src/main/java/br/com/acta/common/config/security/SecurityConfig.java @@ -94,6 +94,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http, FirebaseAuthFi // filtro ainda nao procura usuario no banco, so valida a identidade firebase .requestMatchers(HttpMethod.POST, "/api/v1/auth/ativar").hasAuthority("ROLE_FIREBASE") + .requestMatchers(HttpMethod.POST, "/api/v1/onboarding/inicio").hasAuthority("ROLE_FIREBASE") + .requestMatchers(HttpMethod.POST, "/api/v1/onboarding/gestor").hasAuthority("ROLE_FIREBASE") + .requestMatchers(HttpMethod.GET, "/api/v1/me").hasAuthority("ROLE_FIREBASE") // o restante so segue depois que o filtro montou o usuario autenticado .anyRequest().authenticated() @@ -111,4 +114,4 @@ private void erro(ObjectMapper mapper, HttpServletResponse resp, HttpStatus stat mapper.writeValue(resp.getOutputStream(), new ErroResponse(List.of(msg), status.value())); } -} +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/common/config/swagger/examples/SwaggerOpenapiDescriptions.java b/src/main/java/br/com/acta/common/config/swagger/examples/SwaggerOpenapiDescriptions.java index e78c530..0989f14 100644 --- a/src/main/java/br/com/acta/common/config/swagger/examples/SwaggerOpenapiDescriptions.java +++ b/src/main/java/br/com/acta/common/config/swagger/examples/SwaggerOpenapiDescriptions.java @@ -1,6 +1,16 @@ package br.com.acta.common.config.swagger.examples; public final class SwaggerOpenapiDescriptions { + public static final String ONBOARDING_CONTROLLER = """ + Endpoints responsáveis pelo cadastro de administradores e empresas. + + A API permite: + + - Verificar se a empresa já existe; + - Solicitar acesso como administrador de uma empresa existente; + - Criar uma empresa e seu primeiro administrador. + """; + public static final String AUTH_CONTROLLER = """ Endpoints responsáveis pela autenticação e ativação de usuários. @@ -246,4 +256,4 @@ public final class SwaggerOpenapiDescriptions { (* Algumas operações dependem do status atual do ciclo. Ciclos encerrados, concluídos ou cancelados podem ter sua alteração bloqueada pelas regras de negócio) """; -} \ No newline at end of file +} diff --git a/src/main/java/br/com/acta/controller/AuthController.java b/src/main/java/br/com/acta/controller/AuthController.java index 7054797..1124bd4 100644 --- a/src/main/java/br/com/acta/controller/AuthController.java +++ b/src/main/java/br/com/acta/controller/AuthController.java @@ -33,8 +33,10 @@ public class AuthController implements AuthOpenapi { @GetMapping("/me") @Override - public ResponseEntity me(@AuthenticationPrincipal UsuarioAutenticado usuario) { - return ResponseEntity.ok(mapper.toMeResponse(usuario)); + public ResponseEntity me(@AuthenticationPrincipal FirebaseIdentity identity) { + UsuarioAutenticado usuario = authService.buscarParaMe(identity); + MeResponseDTO dto = mapper.toMeResponse(usuario); + return ResponseEntity.ok(dto); } @PostMapping("/auth/ativar") diff --git a/src/main/java/br/com/acta/service/AuthService.java b/src/main/java/br/com/acta/service/AuthService.java index eb35203..b242509 100644 --- a/src/main/java/br/com/acta/service/AuthService.java +++ b/src/main/java/br/com/acta/service/AuthService.java @@ -1,6 +1,9 @@ package br.com.acta.service; import br.com.acta.common.config.firebase.UsuarioAutenticado; +import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; +import br.com.acta.common.handler.exception.FirebaseAccessRevokedException; +import br.com.acta.dto.auth.AuthMapper; import br.com.acta.repository.padrao.ColaboradorRepository; import br.com.acta.repository.padrao.UsuarioRepository; import jakarta.persistence.EntityManager; @@ -20,6 +23,7 @@ public class AuthService { private final UsuarioRepository repo; private final ColaboradorRepository colaboradorRepo; + private final AuthMapper authMapper; @PersistenceContext private EntityManager entityManager; @@ -33,6 +37,13 @@ public UsuarioAutenticado atual() { return usuario; } + @Transactional(readOnly = true) + public UsuarioAutenticado buscarParaMe(FirebaseIdentity identity) { + return repo.findByFirebaseUid(identity.firebaseUid()) + .map(authMapper::toUsuarioAutenticado) + .orElseThrow(FirebaseAccessRevokedException::new); + } + // só executa se existir uma transação aberta @Transactional(propagation = Propagation.MANDATORY) public void configurarUsuarioAtual() { From 27a1695c66d27f3e913d5b78a24f1bffb15ffd5d Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Sun, 4 Oct 2026 11:13:48 -0300 Subject: [PATCH 5/9] =?UTF-8?q?feat/email:=20implementa=20envio=20de=20con?= =?UTF-8?q?vites=20para=20usu=C3=A1rios=20e=20administradores=20com=20temp?= =?UTF-8?q?lates=20espec=C3=ADficos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/acta/common/client/BrevoClient.java | 47 ++- .../config/swagger/openapi/AuthOpenapi.java | 5 +- .../resources/ValidationMessages.properties | 5 + .../templates/email/convite-admin.html | 297 ++++++++++++++++++ 4 files changed, 337 insertions(+), 17 deletions(-) create mode 100644 src/main/resources/templates/email/convite-admin.html diff --git a/src/main/java/br/com/acta/common/client/BrevoClient.java b/src/main/java/br/com/acta/common/client/BrevoClient.java index 1a08f98..1d7b939 100644 --- a/src/main/java/br/com/acta/common/client/BrevoClient.java +++ b/src/main/java/br/com/acta/common/client/BrevoClient.java @@ -31,20 +31,30 @@ public class BrevoClient { private String url; public void enviarConvite(String email, String nome, OffsetDateTime expiraEm, String token) { - try { - String html = carregarTemplate() - .replace("{{nome}}", HtmlUtils.htmlEscape(nome)) - .replace("{{expiraEm}}", expiraEm.format(DateTimeFormatter.ofPattern("dd/MM/yyyy 'às' HH:mm"))) - .replace("{{logoUrl}}", HtmlUtils.htmlEscape(url.replaceAll("/+$", "") + "/images/acta-logo.png")) - .replace("{{codigo}}", HtmlUtils.htmlEscape(token)); + String html = carregarTemplate("convite-usuario.html") + .replace("{{nome}}", HtmlUtils.htmlEscape(nome)) + .replace("{{expiraEm}}", expiraEm.format(DateTimeFormatter.ofPattern("dd/MM/yyyy 'às' HH:mm"))) + .replace("{{logoUrl}}", HtmlUtils.htmlEscape(url.replaceAll("/+$", "") + "/images/acta-logo.png")) + .replace("{{codigo}}", HtmlUtils.htmlEscape(token)); + + enviarEmail(email, "Convite para acessar o ACTA", html); + } - Map mensagem = Map.of( - "sender", Map.of("name", "ACTA", "email", senderEmail), - "to", List.of(Map.of("email", email)), - "subject", "Convite para acessar o ACTA", - "htmlContent", html - ); + public void enviarConviteAdmin(String email, String nome, String empresa, OffsetDateTime expiraEm, String token) { + String html = carregarTemplate("convite-admin.html") + .replace("{{nome}}", HtmlUtils.htmlEscape(nome)) + .replace("{{empresa}}", HtmlUtils.htmlEscape(empresa)) + .replace("{{expiraEm}}", expiraEm.format(DateTimeFormatter.ofPattern("dd/MM/yyyy 'às' HH:mm"))) + .replace("{{logoUrl}}", HtmlUtils.htmlEscape(url.replaceAll("/+$", "") + "/images/acta-logo.png")) + .replace("{{codigo}}", HtmlUtils.htmlEscape(token)); + + enviarEmail(email, "Convite para administrar uma empresa no ACTA", html); + } + private void enviarEmail(String email, String assunto, String html) { + Map mensagem = montarMensagem(email, assunto, html); + + try { REST_CLIENT.post() .uri("https://api.brevo.com/v3/smtp/email").contentType(MediaType.APPLICATION_JSON) .header("api-key", apiKey) @@ -54,8 +64,17 @@ public void enviarConvite(String email, String nome, OffsetDateTime expiraEm, St } } - private String carregarTemplate() { - try (InputStream input = new ClassPathResource("templates/email/convite-usuario.html").getInputStream()) { + private Map montarMensagem(String email, String assunto, String html) { + return Map.of( + "sender", Map.of("name", "ACTA", "email", senderEmail), + "to", List.of(Map.of("email", email)), + "subject", assunto, + "htmlContent", html + ); + } + + private String carregarTemplate(String template) { + try (InputStream input = new ClassPathResource("templates/email/" + template).getInputStream()) { return new String(input.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException ioe) { throw new BusinessRuleException("Não foi possível carregar o e-mail de convite"); diff --git a/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java b/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java index 894322a..4fa18c5 100644 --- a/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java +++ b/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java @@ -1,7 +1,6 @@ package br.com.acta.common.config.swagger.openapi; import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; -import br.com.acta.common.config.firebase.UsuarioAutenticado; import br.com.acta.common.config.swagger.annotation.ApiAuthenticationResponses; import br.com.acta.common.config.swagger.annotation.ApiBusinessRuleResponse; import br.com.acta.common.config.swagger.annotation.ApiConflictResponse; @@ -25,9 +24,9 @@ public interface AuthOpenapi { @Operation(summary = "Consulta o usuário autenticado") - @ApiResponse(responseCode = "200", description = "Usuário autenticado", content = @Content(schema = @Schema(implementation = MeResponseDTO.class))) + @ApiResponse(responseCode = "200", description = "Usuário autenticado, inclusive quando o cadastro está pendente", content = @Content(schema = @Schema(implementation = MeResponseDTO.class))) @ApiAuthenticationResponses - ResponseEntity me(@Parameter(hidden = true) UsuarioAutenticado usuario); + ResponseEntity me(@Parameter(hidden = true) FirebaseIdentity identity); @Operation(summary = "Ativa o usuário autenticado com código de convite") @ApiResponse(responseCode = "200", description = "Usuário ativado", content = @Content(schema = @Schema(implementation = MeResponseDTO.class))) diff --git a/src/main/resources/ValidationMessages.properties b/src/main/resources/ValidationMessages.properties index 42e55f4..5189606 100644 --- a/src/main/resources/ValidationMessages.properties +++ b/src/main/resources/ValidationMessages.properties @@ -7,6 +7,11 @@ validation.email.size=O e-mail deve ter no máximo {max} caracteres. validation.email.principal.notnull=Informe se este é o e-mail principal. validation.emails.notempty=Os e-mails são obrigatórios. +# ONBOARDING +validation.onboarding.gestor.notnull=Os dados do gestor são obrigatórios. +validation.onboarding.empresa.notnull=Os dados da empresa são obrigatórios. +validation.onboarding.endereco.notnull=O endereço principal da empresa é obrigatório. + # Senha validation.senha.notblank=A senha é obrigatória. validation.senha.size=A senha deve ter entre {min} e {max} caracteres. diff --git a/src/main/resources/templates/email/convite-admin.html b/src/main/resources/templates/email/convite-admin.html new file mode 100644 index 0000000..a5d0731 --- /dev/null +++ b/src/main/resources/templates/email/convite-admin.html @@ -0,0 +1,297 @@ + + + + + + + Convite para administrar o ACTA + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + +
+
+ + \ No newline at end of file From 02c388f5e731a33cc3bc447488fe3079cc34f0c3 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Mon, 5 Oct 2026 11:23:49 -0300 Subject: [PATCH 6/9] =?UTF-8?q?docs:=20detalha=20estados=20do=20perfil,=20?= =?UTF-8?q?auditoria=20inicial=20e=20ajustes=20de=20configura=C3=A7=C3=A3o?= =?UTF-8?q?=20(JDBC=20e=20depend=C3=AAncias)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 +- README.md | 4 +++- pom.xml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index f866dbe..8179633 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -DB_URL=postgresql://localhost:5432/acta +DB_URL=jdbc:postgresql://localhost:5432/acta DB_USER=postgres DB_PASSWORD=123 FIREBASE_PROJECT_ID=seu-projeto-firebase diff --git a/README.md b/README.md index 3a96e43..893c5ff 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,9 @@ Ao executar em contêiner, também disponibilize as credenciais de servidor do F - Público: `GET /api/v1/health`, Swagger UI e OpenAPI. - Demais rotas: `Authorization: Bearer `. -Após o login Firebase, use `POST /auth/ativar` para vincular uma identidade a um usuário ACTA existente, ativo e com e-mail verificado. Consulte `GET /me` para obter o contexto autenticado. +Após o login Firebase, use `GET /me` para consultar o estado do perfil. Uma identidade Firebase válida ainda sem perfil ACTA recebe `estadoPerfil: "CADASTRO_NAO_INICIADO"` e pode iniciar o onboarding; um perfil localizado recebe `estadoPerfil: "PERFIL_CRIADO"` e os dados do usuário. A ausência de perfil não significa sessão inválida. Para vincular um usuário existente, use `POST /auth/ativar` com convite e e-mail verificado. + +Na auditoria do cadastro inicial de empresa, `auditoria.log_auditoria.id_usuario = 0` é reservado para identificar operações iniciadas por uma identidade Firebase verificada antes da existência de um usuário ACTA. A API define `app.current_user_id = 0` apenas na transação e no ramo de criação de uma empresa nova; o UID Firebase permanece vinculado ao usuário criado. O valor `0` não representa um usuário ACTA. > Custom token, refresh token e senha **não** são Firebase ID Tokens e não devem ser usados no cabeçalho `Authorization`. diff --git a/pom.xml b/pom.xml index a9368c6..7f8ac89 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ org.springframework.boot - spring-boot-starter-webmvc + spring-boot-starter-web org.postgresql From c56e2c7a3eac2c9462c2d84d9c03cffb0d5b0c32 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Mon, 5 Oct 2026 11:25:32 -0300 Subject: [PATCH 7/9] feat(auth): atualiza resposta do endpoint /me para incluir estados do perfil --- .../config/swagger/openapi/AuthOpenapi.java | 2 +- .../br/com/acta/controller/AuthController.java | 15 +++------------ .../java/br/com/acta/dto/auth/AuthMapper.java | 3 ++- .../java/br/com/acta/service/AuthService.java | 8 ++++++-- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java b/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java index 4fa18c5..8ce0252 100644 --- a/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java +++ b/src/main/java/br/com/acta/common/config/swagger/openapi/AuthOpenapi.java @@ -24,7 +24,7 @@ public interface AuthOpenapi { @Operation(summary = "Consulta o usuário autenticado") - @ApiResponse(responseCode = "200", description = "Usuário autenticado, inclusive quando o cadastro está pendente", content = @Content(schema = @Schema(implementation = MeResponseDTO.class))) + @ApiResponse(responseCode = "200", description = "Identidade Firebase válida; estadoPerfil informa PERFIL_CRIADO ou CADASTRO_NAO_INICIADO", content = @Content(schema = @Schema(implementation = MeResponseDTO.class))) @ApiAuthenticationResponses ResponseEntity me(@Parameter(hidden = true) FirebaseIdentity identity); diff --git a/src/main/java/br/com/acta/controller/AuthController.java b/src/main/java/br/com/acta/controller/AuthController.java index 1124bd4..2145164 100644 --- a/src/main/java/br/com/acta/controller/AuthController.java +++ b/src/main/java/br/com/acta/controller/AuthController.java @@ -1,42 +1,33 @@ package br.com.acta.controller; import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; -import br.com.acta.common.config.firebase.UsuarioAutenticado; import br.com.acta.common.config.swagger.openapi.AuthOpenapi; import br.com.acta.dto.auth.AtivarRequestDTO; import br.com.acta.dto.auth.AuthMapper; import br.com.acta.dto.auth.ConviteRequestDTO; import br.com.acta.dto.auth.MeResponseDTO; import br.com.acta.dto.core.colaborador.ColaboradorResponseDTO; -import br.com.acta.service.ConviteService; import br.com.acta.service.AuthService; +import br.com.acta.service.ConviteService; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping(value = "/api/v1", produces = MediaType.APPLICATION_JSON_VALUE) @RequiredArgsConstructor public class AuthController implements AuthOpenapi { private final ConviteService conviteService; - private final AuthMapper mapper; private final AuthService authService; @GetMapping("/me") @Override public ResponseEntity me(@AuthenticationPrincipal FirebaseIdentity identity) { - UsuarioAutenticado usuario = authService.buscarParaMe(identity); - MeResponseDTO dto = mapper.toMeResponse(usuario); - return ResponseEntity.ok(dto); + return ResponseEntity.ok(authService.buscarParaMe(identity)); } @PostMapping("/auth/ativar") diff --git a/src/main/java/br/com/acta/dto/auth/AuthMapper.java b/src/main/java/br/com/acta/dto/auth/AuthMapper.java index ff3d9a7..7923ba3 100644 --- a/src/main/java/br/com/acta/dto/auth/AuthMapper.java +++ b/src/main/java/br/com/acta/dto/auth/AuthMapper.java @@ -7,6 +7,7 @@ @Mapper(componentModel = "spring") public interface AuthMapper { + @Mapping(target = "estadoPerfil", constant = "PERFIL_CRIADO") MeResponseDTO toMeResponse(UsuarioAutenticado usuario); @Mapping(source = "id", target = "idUsuario") @@ -16,4 +17,4 @@ public interface AuthMapper { @Mapping(source = "empresa.nome", target = "nomeEmpresa") @Mapping(source = "colaborador.permissaoGestor", target = "permissaoGestor") UsuarioAutenticado toUsuarioAutenticado(Usuario usuario); -} +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/service/AuthService.java b/src/main/java/br/com/acta/service/AuthService.java index b242509..64f772a 100644 --- a/src/main/java/br/com/acta/service/AuthService.java +++ b/src/main/java/br/com/acta/service/AuthService.java @@ -4,6 +4,8 @@ import br.com.acta.common.config.firebase.FirebaseAuthFilter.FirebaseIdentity; import br.com.acta.common.handler.exception.FirebaseAccessRevokedException; import br.com.acta.dto.auth.AuthMapper; +import br.com.acta.entity.enums.EstadoPerfil; +import br.com.acta.dto.auth.MeResponseDTO; import br.com.acta.repository.padrao.ColaboradorRepository; import br.com.acta.repository.padrao.UsuarioRepository; import jakarta.persistence.EntityManager; @@ -38,10 +40,12 @@ public UsuarioAutenticado atual() { } @Transactional(readOnly = true) - public UsuarioAutenticado buscarParaMe(FirebaseIdentity identity) { + public MeResponseDTO buscarParaMe(FirebaseIdentity identity) { + if (identity == null) throw new FirebaseAccessRevokedException(); return repo.findByFirebaseUid(identity.firebaseUid()) .map(authMapper::toUsuarioAutenticado) - .orElseThrow(FirebaseAccessRevokedException::new); + .map(authMapper::toMeResponse) + .orElseGet(() -> new MeResponseDTO(identity.firebaseUid(), null, null, null, null, identity.email(), null, null, false, null, EstadoPerfil.CADASTRO_NAO_INICIADO)); } // só executa se existir uma transação aberta From 699ca601d3a4f453f73506181c5ddd449b458525 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Mon, 5 Oct 2026 11:27:14 -0300 Subject: [PATCH 8/9] =?UTF-8?q?feat/onboarding:=20adiciona=20enum=20Estado?= =?UTF-8?q?Perfil=20e=20atualiza=20fluxo=20de=20cria=C3=A7=C3=A3o=20de=20u?= =?UTF-8?q?su=C3=A1rios=20e=20empresas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/openapi/OnboardingOpenapi.java | 3 +- .../br/com/acta/dto/auth/MeResponseDTO.java | 6 ++-- .../com/acta/entity/enums/EstadoPerfil.java | 6 ++++ .../com/acta/service/OnboardingService.java | 35 +++++++++++++------ 4 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 src/main/java/br/com/acta/entity/enums/EstadoPerfil.java diff --git a/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java b/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java index 89ba9d0..2917d53 100644 --- a/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java +++ b/src/main/java/br/com/acta/common/config/swagger/openapi/OnboardingOpenapi.java @@ -26,9 +26,10 @@ public interface OnboardingOpenapi { ResponseEntity iniciar(@Parameter(hidden = true) FirebaseIdentity identity, @RequestBody(description = "Dados do gestor e CNPJ da empresa", required = true, content = @Content(schema = @Schema(implementation = OnboardingInicioRequestDTO.class))) OnboardingInicioRequestDTO dto); @Operation(summary = "Cadastra a empresa e seu primeiro gestor") - @ApiResponse(responseCode = "201", description = "Cadastro criado aguardando liberação", content = @Content(schema = @Schema(implementation = OnboardingResponseDTO.class))) + @ApiResponse(responseCode = "201", description = "Empresa criada e primeiro gestor ativado", content = @Content(schema = @Schema(implementation = OnboardingResponseDTO.class))) @ApiAuthenticationResponses @ApiBadRequestResponse @ApiConflictResponse ResponseEntity cadastrarEmpresa(@Parameter(hidden = true) FirebaseIdentity identity, @RequestBody(description = "Dados do gestor, empresa e endereço principal", required = true, content = @Content(schema = @Schema(implementation = OnboardingRequestDTO.class))) OnboardingRequestDTO dto); + } \ No newline at end of file diff --git a/src/main/java/br/com/acta/dto/auth/MeResponseDTO.java b/src/main/java/br/com/acta/dto/auth/MeResponseDTO.java index 90faead..565f429 100644 --- a/src/main/java/br/com/acta/dto/auth/MeResponseDTO.java +++ b/src/main/java/br/com/acta/dto/auth/MeResponseDTO.java @@ -1,5 +1,6 @@ package br.com.acta.dto.auth; +import br.com.acta.entity.enums.EstadoPerfil; import br.com.acta.entity.enums.StatusGeral; import br.com.acta.entity.enums.TipoUsuario; @@ -13,5 +14,6 @@ public record MeResponseDTO( String nomeEmpresa, TipoUsuario tipo, boolean permissaoGestor, - StatusGeral status -) { } + StatusGeral status, + EstadoPerfil estadoPerfil +) { } \ No newline at end of file diff --git a/src/main/java/br/com/acta/entity/enums/EstadoPerfil.java b/src/main/java/br/com/acta/entity/enums/EstadoPerfil.java new file mode 100644 index 0000000..c443f60 --- /dev/null +++ b/src/main/java/br/com/acta/entity/enums/EstadoPerfil.java @@ -0,0 +1,6 @@ +package br.com.acta.entity.enums; + +public enum EstadoPerfil { + CADASTRO_NAO_INICIADO, + PERFIL_CRIADO +} \ No newline at end of file diff --git a/src/main/java/br/com/acta/service/OnboardingService.java b/src/main/java/br/com/acta/service/OnboardingService.java index 2351e64..53cd8df 100644 --- a/src/main/java/br/com/acta/service/OnboardingService.java +++ b/src/main/java/br/com/acta/service/OnboardingService.java @@ -31,6 +31,8 @@ import br.com.acta.repository.padrao.EmpresaRepository; import br.com.acta.repository.padrao.UsuarioRepository; import br.com.caelum.stella.validation.CNPJValidator; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; @@ -55,6 +57,9 @@ public class OnboardingService { private final BrevoClient brevoClient; private final CNPJValidator cnpjValidator = new CNPJValidator(); + @PersistenceContext + private EntityManager entityManager; + @PreAuthorize("hasAuthority('ROLE_FIREBASE')") @Transactional public OnboardingResponseDTO iniciar(FirebaseIdentity identity, OnboardingInicioRequestDTO dto) { @@ -67,13 +72,18 @@ public OnboardingResponseDTO iniciar(FirebaseIdentity identity, OnboardingInicio Empresa empresa = empresaRepo.findByCnpj(dto.cnpj()).orElse(null); if (empresa == null) return onboardingMapper.solicitarDadosEmpresa(); + // Empresas pendentes não têm administrador ativo para um convite + if (empresa.getStatus() == StatusGeral.PENDENTE) { + return new OnboardingResponseDTO(true, false, false, empresa.getId(), empresa.getStatus(), null, null, null); + } + Usuario criadoPor = usuarioRepo.findByTipoAndStatusAndEmpresaId(TipoUsuario.ADMIN, StatusGeral.ATIVO, empresa.getId()) .stream() .findFirst() .orElseThrow(() -> new BusinessRuleException("A empresa não possui um administrador ativo")); - Usuario usuario = criarUsuarioPendente(dto.gestor(), identity.email(), empresa); - Colaborador colaborador = criarColaboradorPendente(dto.gestor(), identity.email(), empresa, usuario); + Usuario usuario = criarUsuario(dto.gestor(), identity.email(), empresa, null, StatusGeral.PENDENTE); + Colaborador colaborador = criarColaborador(dto.gestor(), identity.email(), empresa, usuario, StatusGeral.PENDENTE); usuario.setColaborador(colaborador); String codigo = TokenUtils.gerarCodigo(); @@ -102,6 +112,8 @@ public OnboardingResponseDTO cadastrarEmpresa(FirebaseIdentity identity, Onboard if (usuarioRepo.existsByEmailLoginIgnoreCase(identity.email())) throw new UniqueViolationException("E-mail"); if (colaboradorRepo.existsByCpf(dto.gestor().cpf())) throw new UniqueViolationException("CPF"); + configurarAuditoriaOnboardingInicial(); + EmpresaRequestDTO empresaRequest = new EmpresaRequestDTO( dto.empresa().cnpj(), dto.empresa().nome(), @@ -120,11 +132,11 @@ public OnboardingResponseDTO cadastrarEmpresa(FirebaseIdentity identity, Onboard true))); Empresa empresa = empresaMapper.toEntity(empresaRequest); - empresa.setStatus(StatusGeral.PENDENTE); + empresa.setStatus(StatusGeral.ATIVO); Empresa empresaSalva = empresaRepo.save(empresa); - Usuario usuario = criarUsuarioPendente(dto.gestor(), identity.email(), empresaSalva, identity.firebaseUid()); - Colaborador colaborador = criarColaboradorPendente(dto.gestor(), identity.email(), empresaSalva, usuario); + Usuario usuario = criarUsuario(dto.gestor(), identity.email(), empresaSalva, identity.firebaseUid(), StatusGeral.ATIVO); + Colaborador colaborador = criarColaborador(dto.gestor(), identity.email(), empresaSalva, usuario, StatusGeral.ATIVO); usuario.setColaborador(colaborador); return onboardingMapper.toResponse(usuario, false, false, false); @@ -135,19 +147,20 @@ private void validarIdentidade(FirebaseIdentity identity, String emailGestor) { throw new FirebaseAccessRevokedException(); } - private Usuario criarUsuarioPendente(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa) { - return criarUsuarioPendente(gestor, email, empresa, null); + private void configurarAuditoriaOnboardingInicial() { + // ID 0 é o ator reservado aos eventos do onboarding sem perfil ACTA + entityManager.createNativeQuery("SELECT set_config('app.current_user_id', '0', true)").getSingleResult(); } - private Usuario criarUsuarioPendente(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa, String firebaseUid) { + private Usuario criarUsuario(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa, String firebaseUid, StatusGeral status) { UsuarioRequestDTO request = new UsuarioRequestDTO(gestor.nome(), email.trim(), firebaseUid, TipoUsuario.ADMIN); Usuario usuario = usuarioMapper.toEntity(request); - usuario.setStatus(StatusGeral.PENDENTE); + usuario.setStatus(status); usuario.setEmpresa(empresa); return usuarioRepo.save(usuario); } - private Colaborador criarColaboradorPendente(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa, Usuario usuario) { + private Colaborador criarColaborador(OnboardingRequestDTO.Gestor gestor, String email, Empresa empresa, Usuario usuario, StatusGeral status) { UsuarioRequestDTO usuarioRequest = new UsuarioRequestDTO(gestor.nome(), email, usuario.getFirebaseUid(), TipoUsuario.ADMIN); ColaboradorRequestDTO request = new ColaboradorRequestDTO( gestor.cpf(), @@ -163,7 +176,7 @@ private Colaborador criarColaboradorPendente(OnboardingRequestDTO.Gestor gestor, usuarioRequest); Colaborador colaborador = colaboradorMapper.toEntity(request); - colaborador.setStatus(StatusGeral.PENDENTE); + colaborador.setStatus(status); colaborador.setEmpresa(empresa); colaborador.setUsuario(usuario); return colaboradorRepo.save(colaborador); From 4664050e126f22d195a0cfbec7f0db009e646200 Mon Sep 17 00:00:00 2001 From: Davi Aliaga Date: Tue, 6 Oct 2026 07:51:33 -0300 Subject: [PATCH 9/9] =?UTF-8?q?feat/onboarding:=20adiciona=20auditoria=20i?= =?UTF-8?q?nicial=20ao=20fluxo=20de=20cria=C3=A7=C3=A3o=20de=20usu=C3=A1ri?= =?UTF-8?q?os=20e=20empresas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/br/com/acta/service/OnboardingService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/br/com/acta/service/OnboardingService.java b/src/main/java/br/com/acta/service/OnboardingService.java index 53cd8df..77415a2 100644 --- a/src/main/java/br/com/acta/service/OnboardingService.java +++ b/src/main/java/br/com/acta/service/OnboardingService.java @@ -69,6 +69,8 @@ public OnboardingResponseDTO iniciar(FirebaseIdentity identity, OnboardingInicio if (colaboradorRepo.existsByCpf(dto.gestor().cpf())) throw new UniqueViolationException("CPF"); if (!cnpjValidator.isEligible(dto.cnpj())) throw new RegexException("CNPJ"); + configurarAuditoriaOnboardingInicial(); + Empresa empresa = empresaRepo.findByCnpj(dto.cnpj()).orElse(null); if (empresa == null) return onboardingMapper.solicitarDadosEmpresa();