feat: otimização de performance e ajustes finais
This commit is contained in:
@@ -0,0 +1,986 @@
|
||||
# 🔬 Análise Ultra Profunda: Módulo RH & Folha de Pagamento
|
||||
## Sistema de Gestão de Função Pública (SIGEFP)
|
||||
|
||||
**Data:** 2025-01-27
|
||||
**Objetivo:** Análise extremamente detalhada para identificar TODOS os problemas, gaps, edge cases e oportunidades de melhoria que possam ter passado despercebidos
|
||||
|
||||
---
|
||||
|
||||
## 📋 Índice
|
||||
|
||||
1. [Análise de Validações de Negócio](#1-análise-de-validações-de-negócio)
|
||||
2. [Análise de Edge Cases e Boundary Conditions](#2-análise-de-edge-cases-e-boundary-conditions)
|
||||
3. [Análise de Integridade de Dados](#3-análise-de-integridade-de-dados)
|
||||
4. [Análise de Concorrência e Race Conditions](#4-análise-de-concorrência-e-race-conditions)
|
||||
5. [Análise de Validações de Conformidade Legal](#5-análise-de-validações-de-conformidade-legal)
|
||||
6. [Análise de Performance e Otimizações](#6-análise-de-performance-e-otimizações)
|
||||
7. [Análise de Tratamento de Erros](#7-análise-de-tratamento-de-erros)
|
||||
8. [Análise de Segurança](#8-análise-de-segurança)
|
||||
9. [Análise de Frontend](#9-análise-de-frontend)
|
||||
10. [Análise de Testes](#10-análise-de-testes)
|
||||
11. [Problemas Críticos Identificados](#11-problemas-críticos-identificados)
|
||||
12. [Recomendações Prioritárias](#12-recomendações-prioritárias)
|
||||
|
||||
---
|
||||
|
||||
## 1. Análise de Validações de Negócio
|
||||
|
||||
### 1.1 Validações Faltantes em `PayrollService`
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 1: Falta validação de duplicidade de PayrollRun**
|
||||
|
||||
**Localização:** `PayrollService.createPayrollRun()`
|
||||
|
||||
**Problema:**
|
||||
```java
|
||||
// Código atual - NÃO valida duplicidade
|
||||
public PayrollRunDTO createPayrollRun(CreatePayrollRunDTO dto) {
|
||||
PayrollPeriod period = payrollPeriodRepository.findById(dto.getPeriodId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Período não encontrado"));
|
||||
|
||||
PayrollRun payrollRun = PayrollRun.builder()
|
||||
.period(period)
|
||||
.ministry(dto.getMinistryId())
|
||||
.orgUnit(dto.getOrgUnitId())
|
||||
.runType(dto.getRunType())
|
||||
.status("PENDING")
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
PayrollRun saved = payrollRunRepository.save(payrollRun);
|
||||
return toRunDTO(saved);
|
||||
}
|
||||
```
|
||||
|
||||
**Impacto:** Permite criar múltiplas execuções de folha para o mesmo período, ministério e unidade orgânica, causando:
|
||||
- Duplicação de pagamentos
|
||||
- Inconsistências orçamentárias
|
||||
- Problemas de auditoria
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
// Verificar se já existe PayrollRun para o mesmo período/ministério/orgUnit
|
||||
if (dto.getMinistryId() != null && dto.getOrgUnitId() != null) {
|
||||
List<PayrollRun> existing = payrollRunRepository.findByPeriodIdAndMinistry(
|
||||
dto.getPeriodId(), dto.getMinistryId());
|
||||
|
||||
boolean duplicate = existing.stream()
|
||||
.anyMatch(run ->
|
||||
Objects.equals(run.getOrgUnit(), dto.getOrgUnitId()) &&
|
||||
Objects.equals(run.getRunType(), dto.getRunType()) &&
|
||||
!"CLOSED".equals(run.getStatus())
|
||||
);
|
||||
|
||||
if (duplicate) {
|
||||
throw new BusinessException(
|
||||
"Já existe uma execução de folha ativa para este período, ministério e unidade orgânica",
|
||||
"DUPLICATE_PAYROLL_RUN",
|
||||
HttpStatus.CONFLICT
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 2: Falta validação de agentes elegíveis para folha**
|
||||
|
||||
**Localização:** `PayrollService.generatePayrollItems()`
|
||||
|
||||
**Problema:**
|
||||
```java
|
||||
// Código atual - Busca apenas por status "ACTIVE"
|
||||
List<Agent> activeAgents = agentRepository.findByStatus("ACTIVE");
|
||||
|
||||
for (Agent agent : activeAgents) {
|
||||
// Regra: Deve ter posse e escalão salarial
|
||||
if (agent.getSalaryStep() == null || agent.getPosseDate() == null) {
|
||||
log.warn("Agente {} ignorado...");
|
||||
continue; // Apenas log, não valida outros critérios
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Faltam validações para:**
|
||||
1. **Contrato ativo:** Não verifica se o agente tem `AgentContract` ativo
|
||||
2. **Data de admissão:** Não verifica se o agente foi admitido antes do período da folha
|
||||
3. **Data de término:** Não verifica se o agente foi desligado durante o período
|
||||
4. **Período probatório:** Não considera se está em período probatório (pode ter regras diferentes)
|
||||
5. **Suspensão:** Não verifica se o agente está suspenso durante o período
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
private boolean isAgentEligibleForPayroll(Agent agent, PayrollPeriod period) {
|
||||
// 1. Verificar contrato ativo
|
||||
Optional<AgentContract> activeContract = agentContractRepository
|
||||
.findByAgentAndIsActiveTrue(agent);
|
||||
if (activeContract.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AgentContract contract = activeContract.get();
|
||||
|
||||
// 2. Verificar se contrato está vigente no período
|
||||
if (contract.getStartDate().isAfter(period.getEndDate())) {
|
||||
return false; // Contrato inicia após o período
|
||||
}
|
||||
|
||||
if (contract.getEndDate() != null &&
|
||||
contract.getEndDate().isBefore(period.getStartDate())) {
|
||||
return false; // Contrato terminou antes do período
|
||||
}
|
||||
|
||||
// 3. Verificar se agente não foi desligado durante o período
|
||||
if (agent.getTerminationDate() != null &&
|
||||
agent.getTerminationDate().isBefore(period.getEndDate()) &&
|
||||
agent.getTerminationDate().isAfter(period.getStartDate())) {
|
||||
// Agente foi desligado durante o período - calcular proporcional
|
||||
// (mas isso é outro problema - ver seção 2.1)
|
||||
}
|
||||
|
||||
// 4. Verificar se não está suspenso
|
||||
if ("SUSPENDED".equals(agent.getStatus())) {
|
||||
// Verificar se suspensão está dentro do período
|
||||
// (precisa de entidade AgentSuspension ou campo em AgentStatusHistory)
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 3: Falta validação de período fechado**
|
||||
|
||||
**Localização:** `PayrollService.createPayrollRun()`
|
||||
|
||||
**Problema:** Permite criar PayrollRun para período com status "CLOSED"
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
if (!"OPEN".equals(period.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Não é possível criar execução de folha para período fechado",
|
||||
"PERIOD_CLOSED",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Validações Faltantes em `AgentService`
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 4: Validação de promoção incompleta**
|
||||
|
||||
**Localização:** `AgentService.validatePromotion()`
|
||||
|
||||
**Problema:**
|
||||
```java
|
||||
private void validatePromotion(Agent agent) {
|
||||
// Regra do Decreto 12-A/94: Avaliação de desempenho de, no mínimo, "Bom" nos últimos três anos.
|
||||
int currentYear = LocalDate.now().getYear();
|
||||
List<PerformanceEvaluation> evals = performanceEvaluationRepository
|
||||
.findByAgentIdAndReferenceYearBetweenOrderByReferenceYearDesc(
|
||||
agent.getId(), currentYear - 3, currentYear - 1);
|
||||
|
||||
if (evals.size() < 3) {
|
||||
throw new IllegalStateException(
|
||||
"O agente deve ter pelo menos 3 anos de avaliações de desempenho para ser promovido.");
|
||||
}
|
||||
|
||||
for (PerformanceEvaluation eval : evals) {
|
||||
if (eval.getScore() < 14) { // 14 é o mínimo para "Bom" na escala 0-20
|
||||
throw new IllegalStateException(
|
||||
"O agente não cumpre o requisito de avaliação 'Bom' (mínimo 14 pontos) no ano "
|
||||
+ eval.getReferenceYear());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Faltam validações:**
|
||||
1. **Tempo mínimo no escalão atual:** Decreto 12-A/94 exige tempo mínimo (geralmente 3 anos) no escalão antes de poder progredir
|
||||
2. **Tempo mínimo na categoria:** Para promoção (mudança de categoria), exige tempo mínimo na categoria atual
|
||||
3. **Habilitação literária:** Verificar se a nova categoria requer habilitação literária superior
|
||||
4. **Vagas disponíveis:** Verificar se há vaga na categoria/grau de destino
|
||||
5. **Status da avaliação:** Verificar se as avaliações estão com status "FINAL" (não apenas "DRAFT")
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
private void validatePromotion(Agent agent, UUID newCategory, UUID newGrade, UUID newStep) {
|
||||
// 1. Validar avaliações (código existente)
|
||||
validatePromotion(agent);
|
||||
|
||||
// 2. Validar tempo mínimo no escalão atual
|
||||
if (agent.getSalaryStep() != null) {
|
||||
// Buscar último CareerEvent de progressão/promoção
|
||||
List<CareerEvent> events = careerEventRepository
|
||||
.findByAgentIdOrderByEffectiveDateDesc(agent.getId());
|
||||
|
||||
Optional<CareerEvent> lastProgression = events.stream()
|
||||
.filter(e -> e.getEventType() == CareerEventType.PROGRESSAO ||
|
||||
e.getEventType() == CareerEventType.PROMOCAO)
|
||||
.findFirst();
|
||||
|
||||
if (lastProgression.isPresent()) {
|
||||
LocalDate lastProgressionDate = lastProgression.get().getEffectiveDate();
|
||||
long yearsInStep = ChronoUnit.YEARS.between(lastProgressionDate, LocalDate.now());
|
||||
|
||||
if (yearsInStep < 3) {
|
||||
throw new BusinessException(
|
||||
String.format("Tempo mínimo de 3 anos no escalão atual não cumprido. " +
|
||||
"Última progressão: %s (%d anos atrás)",
|
||||
lastProgressionDate, yearsInStep),
|
||||
"MINIMUM_TIME_NOT_MET",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Validar habilitação literária (se mudança de categoria)
|
||||
if (newCategory != null && !Objects.equals(newCategory, agent.getSalaryCategory())) {
|
||||
// Buscar requisitos da nova categoria
|
||||
SalaryCategory category = salaryCategoryRepository.findById(newCategory)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Categoria não encontrada"));
|
||||
|
||||
// Verificar se agente tem habilitação literária adequada
|
||||
// (precisa de campo em SalaryCategory para requisitos)
|
||||
}
|
||||
|
||||
// 4. Validar status das avaliações
|
||||
List<PerformanceEvaluation> evals = performanceEvaluationRepository
|
||||
.findByAgentIdAndReferenceYearBetweenOrderByReferenceYearDesc(
|
||||
agent.getId(), LocalDate.now().getYear() - 3, LocalDate.now().getYear() - 1);
|
||||
|
||||
boolean hasDraftEval = evals.stream()
|
||||
.anyMatch(e -> "DRAFT".equals(e.getStatus()));
|
||||
|
||||
if (hasDraftEval) {
|
||||
throw new BusinessException(
|
||||
"Todas as avaliações devem estar finalizadas para promoção",
|
||||
"DRAFT_EVALUATIONS",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 5: Falta validação de sobreposição de contratos**
|
||||
|
||||
**Localização:** `AgentContractService.saveContract()`
|
||||
|
||||
**Problema:** Permite criar contratos com datas sobrepostas
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
private void validateContractDates(Agent agent, AgentContract newContract) {
|
||||
List<AgentContract> existingContracts = contractRepository.findByAgentId(agent.getId());
|
||||
|
||||
for (AgentContract existing : existingContracts) {
|
||||
if (existing.getId().equals(newContract.getId())) {
|
||||
continue; // Ignorar o próprio contrato se for update
|
||||
}
|
||||
|
||||
// Verificar sobreposição
|
||||
boolean overlaps = !(
|
||||
(newContract.getEndDate() != null &&
|
||||
newContract.getEndDate().isBefore(existing.getStartDate())) ||
|
||||
(existing.getEndDate() != null &&
|
||||
existing.getEndDate().isBefore(newContract.getStartDate()))
|
||||
);
|
||||
|
||||
if (overlaps) {
|
||||
throw new BusinessException(
|
||||
String.format("Contrato sobrepõe com contrato existente: %s a %s",
|
||||
existing.getStartDate(),
|
||||
existing.getEndDate() != null ? existing.getEndDate() : "indefinido"),
|
||||
"CONTRACT_OVERLAP",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Validações Faltantes em `TaxService`
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 6: Falta validação de sobreposição de regras globais**
|
||||
|
||||
**Localização:** `TaxService.saveRule()`
|
||||
|
||||
**Problema:**
|
||||
```java
|
||||
public GlobalDeductionRule saveRule(GlobalDeductionRule rule) {
|
||||
// Futuro: Adicionar validações de sobreposição de datas
|
||||
return globalDeductionRuleRepository.save(rule);
|
||||
}
|
||||
```
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
public GlobalDeductionRule saveRule(GlobalDeductionRule rule) {
|
||||
// Validar sobreposição de datas para o mesmo DeductionType
|
||||
List<GlobalDeductionRule> existingRules = globalDeductionRuleRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.filter(r -> r.getDeductionType().getId().equals(rule.getDeductionType().getId()))
|
||||
.filter(r -> !r.getId().equals(rule.getId())) // Excluir a própria regra se for update
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (GlobalDeductionRule existing : existingRules) {
|
||||
boolean overlaps = !(
|
||||
(rule.getValidTo() != null &&
|
||||
rule.getValidTo().isBefore(existing.getValidFrom())) ||
|
||||
(existing.getValidTo() != null &&
|
||||
existing.getValidTo().isBefore(rule.getValidFrom()))
|
||||
);
|
||||
|
||||
if (overlaps && existing.getActive()) {
|
||||
throw new BusinessException(
|
||||
String.format("Regra global sobrepõe com regra existente para %s: %s a %s",
|
||||
rule.getDeductionType().getName(),
|
||||
existing.getValidFrom(),
|
||||
existing.getValidTo() != null ? existing.getValidTo() : "indefinido"),
|
||||
"RULE_OVERLAP",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return globalDeductionRuleRepository.save(rule);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Análise de Edge Cases e Boundary Conditions
|
||||
|
||||
### 2.1 Cálculo Proporcional de Salário
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 7: Falta cálculo proporcional para agentes admitidos/desligados durante o período**
|
||||
|
||||
**Localização:** `PayrollService.generatePayrollItems()`
|
||||
|
||||
**Problema:** Se um agente foi admitido no dia 15 do mês, recebe salário integral. Se foi desligado no dia 20, também recebe salário integral.
|
||||
|
||||
**Cenários não tratados:**
|
||||
1. Agente admitido durante o período → Deve receber proporcional
|
||||
2. Agente desligado durante o período → Deve receber proporcional
|
||||
3. Agente suspenso durante o período → Deve ter desconto proporcional
|
||||
4. Agente em período probatório → Pode ter regras diferentes
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
private BigDecimal calculateProportionalSalary(
|
||||
Agent agent,
|
||||
PayrollPeriod period,
|
||||
BigDecimal baseAmount) {
|
||||
|
||||
LocalDate periodStart = period.getStartDate();
|
||||
LocalDate periodEnd = period.getEndDate();
|
||||
LocalDate effectiveStart = periodStart;
|
||||
LocalDate effectiveEnd = periodEnd;
|
||||
|
||||
// Verificar data de admissão
|
||||
if (agent.getHireDate() != null && agent.getHireDate().isAfter(periodStart)) {
|
||||
effectiveStart = agent.getHireDate();
|
||||
}
|
||||
|
||||
// Verificar data de posse (se diferente de admissão)
|
||||
if (agent.getPosseDate() != null && agent.getPosseDate().isAfter(effectiveStart)) {
|
||||
effectiveStart = agent.getPosseDate();
|
||||
}
|
||||
|
||||
// Verificar data de término
|
||||
if (agent.getTerminationDate() != null &&
|
||||
agent.getTerminationDate().isBefore(periodEnd) &&
|
||||
agent.getTerminationDate().isAfter(periodStart)) {
|
||||
effectiveEnd = agent.getTerminationDate();
|
||||
}
|
||||
|
||||
// Verificar contrato
|
||||
Optional<AgentContract> contract = agentContractRepository
|
||||
.findByAgentAndIsActiveTrue(agent);
|
||||
if (contract.isPresent()) {
|
||||
AgentContract c = contract.get();
|
||||
if (c.getStartDate().isAfter(effectiveStart)) {
|
||||
effectiveStart = c.getStartDate();
|
||||
}
|
||||
if (c.getEndDate() != null && c.getEndDate().isBefore(effectiveEnd)) {
|
||||
effectiveEnd = c.getEndDate();
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular dias efetivos
|
||||
long totalDays = ChronoUnit.DAYS.between(periodStart, periodEnd) + 1;
|
||||
long effectiveDays = ChronoUnit.DAYS.between(effectiveStart, effectiveEnd) + 1;
|
||||
|
||||
if (effectiveDays == totalDays) {
|
||||
return baseAmount; // Período completo
|
||||
}
|
||||
|
||||
// Calcular proporcional
|
||||
BigDecimal proportional = baseAmount
|
||||
.multiply(new BigDecimal(effectiveDays))
|
||||
.divide(new BigDecimal(totalDays), 2, RoundingMode.HALF_UP);
|
||||
|
||||
log.info("Salário proporcional calculado para agente {}: {} dias de {} ({}%)",
|
||||
agent.getMatricula(), effectiveDays, totalDays,
|
||||
proportional.divide(baseAmount, 4, RoundingMode.HALF_UP).multiply(new BigDecimal(100)));
|
||||
|
||||
return proportional;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Cálculo de Faltas
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 8: Cálculo de faltas assume 30 dias fixos**
|
||||
|
||||
**Localização:** `PayrollService.generatePayrollItems()`
|
||||
|
||||
**Problema:**
|
||||
```java
|
||||
// Correção: Dias exatos do período
|
||||
long daysInPeriod = java.time.temporal.ChronoUnit.DAYS.between(pStart, pEnd) + 1;
|
||||
if (daysInPeriod == 0)
|
||||
daysInPeriod = 30; // Fallback
|
||||
```
|
||||
|
||||
**Problemas:**
|
||||
1. Fallback de 30 dias é incorreto (fevereiro tem 28/29, meses têm 30/31)
|
||||
2. Não considera dias úteis vs. dias corridos
|
||||
3. Não considera feriados
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
private long calculateWorkingDaysInPeriod(LocalDate start, LocalDate end) {
|
||||
long days = ChronoUnit.DAYS.between(start, end) + 1;
|
||||
|
||||
// Se days == 0, algo está errado - lançar exceção
|
||||
if (days <= 0) {
|
||||
throw new BusinessException(
|
||||
"Período inválido: data de início deve ser anterior à data de fim",
|
||||
"INVALID_PERIOD",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
// Contar dias úteis (excluir sábados e domingos)
|
||||
// TODO: Adicionar tabela de feriados
|
||||
long workingDays = 0;
|
||||
LocalDate current = start;
|
||||
while (!current.isAfter(end)) {
|
||||
DayOfWeek dayOfWeek = current.getDayOfWeek();
|
||||
if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY) {
|
||||
workingDays++;
|
||||
}
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
|
||||
return workingDays;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Cálculo de Impostos Progressivos
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 9: Falta validação de sobreposição de escalões de imposto**
|
||||
|
||||
**Localização:** `PayrollService.generatePayrollItems()`
|
||||
|
||||
**Problema:** Se houver escalões sobrepostos ou gaps, o cálculo pode ser incorreto.
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
private void validateTaxBrackets(List<TaxBracket> brackets) {
|
||||
if (brackets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ordenar por lowerLimit
|
||||
brackets.sort(Comparator.comparing(TaxBracket::getLowerLimit));
|
||||
|
||||
// Verificar gaps e sobreposições
|
||||
for (int i = 0; i < brackets.size() - 1; i++) {
|
||||
TaxBracket current = brackets.get(i);
|
||||
TaxBracket next = brackets.get(i + 1);
|
||||
|
||||
BigDecimal currentUpper = current.getUpperLimit() != null ?
|
||||
current.getUpperLimit() : new BigDecimal("999999999");
|
||||
BigDecimal nextLower = next.getLowerLimit();
|
||||
|
||||
// Verificar gap
|
||||
if (currentUpper.compareTo(nextLower) < 0) {
|
||||
log.warn("Gap detectado entre escalões: {} a {}",
|
||||
currentUpper, nextLower);
|
||||
}
|
||||
|
||||
// Verificar sobreposição
|
||||
if (currentUpper.compareTo(nextLower) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Sobreposição detectada entre escalões: %s-%s e %s-%s",
|
||||
current.getLowerLimit(), currentUpper,
|
||||
nextLower, next.getUpperLimit()),
|
||||
"TAX_BRACKET_OVERLAP",
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Análise de Integridade de Dados
|
||||
|
||||
### 3.1 Constraints de Banco de Dados
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 10: Falta constraint UNIQUE para PayrollRun**
|
||||
|
||||
**Problema:** Não há constraint que impeça duplicidade de PayrollRun para o mesmo período/ministério/orgUnit.
|
||||
|
||||
**Solução SQL:**
|
||||
```sql
|
||||
-- Adicionar constraint única
|
||||
ALTER TABLE payroll_run
|
||||
ADD CONSTRAINT uk_payroll_run_unique
|
||||
UNIQUE (period_id, ministry_id, org_unit_id, run_type, status)
|
||||
WHERE status != 'CLOSED';
|
||||
```
|
||||
|
||||
**Nota:** PostgreSQL não suporta `WHERE` em `UNIQUE`, então usar índice parcial:
|
||||
```sql
|
||||
CREATE UNIQUE INDEX uk_payroll_run_unique_active
|
||||
ON payroll_run (period_id, ministry_id, org_unit_id, run_type)
|
||||
WHERE status != 'CLOSED';
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 11: Falta constraint CHECK para datas**
|
||||
|
||||
**Problema:** Não há validação de que `startDate <= endDate` em várias entidades.
|
||||
|
||||
**Solução SQL:**
|
||||
```sql
|
||||
-- PayrollPeriod
|
||||
ALTER TABLE payroll_period
|
||||
ADD CONSTRAINT chk_period_dates
|
||||
CHECK (start_date <= end_date);
|
||||
|
||||
-- AgentContract
|
||||
ALTER TABLE agent_contract
|
||||
ADD CONSTRAINT chk_contract_dates
|
||||
CHECK (end_date IS NULL OR start_date <= end_date);
|
||||
|
||||
-- Absence
|
||||
ALTER TABLE absence
|
||||
ADD CONSTRAINT chk_absence_dates
|
||||
CHECK (start_date <= end_date);
|
||||
|
||||
-- GlobalDeductionRule
|
||||
ALTER TABLE global_deduction_rule
|
||||
ADD CONSTRAINT chk_rule_dates
|
||||
CHECK (valid_to IS NULL OR valid_from <= valid_to);
|
||||
|
||||
-- TaxBracket
|
||||
ALTER TABLE tax_bracket
|
||||
ADD CONSTRAINT chk_bracket_limits
|
||||
CHECK (upper_limit IS NULL OR lower_limit <= upper_limit);
|
||||
```
|
||||
|
||||
### 3.2 Sincronização de Dados
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 12: Falta sincronização de `deductedInPayrollRunId` em Absence**
|
||||
|
||||
**Localização:** `PayrollService.generatePayrollItems()`
|
||||
|
||||
**Problema:** Quando uma falta é deduzida na folha, o campo `deductedInPayrollRunId` não é atualizado.
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
// Após criar PayrollItem de falta
|
||||
PayrollItem absenceItem = PayrollItem.builder()
|
||||
// ...
|
||||
.build();
|
||||
payrollItemRepository.save(absenceItem);
|
||||
|
||||
// Atualizar ausência
|
||||
for (Absence abs : legacyAbsences) {
|
||||
if (!abs.isJustified() && abs.getDeductedInPayrollRunId() == null) {
|
||||
// Verificar se a falta está dentro do período
|
||||
if (!abs.getStartDate().isAfter(pEnd) &&
|
||||
!abs.getEndDate().isBefore(pStart)) {
|
||||
abs.setDeductedInPayrollRunId(payrollRunId);
|
||||
absenceRepository.save(abs);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Análise de Concorrência e Race Conditions
|
||||
|
||||
### 4.1 Problemas de Concorrência
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 13: Falta controle de concorrência em PayrollRun**
|
||||
|
||||
**Problema:** Múltiplos usuários podem processar a mesma folha simultaneamente.
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
@Transactional
|
||||
public void processPayrollRun(UUID payrollRunId) {
|
||||
// Usar lock pessimista
|
||||
PayrollRun payrollRun = payrollRunRepository.findById(payrollRunId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Execução não encontrada"));
|
||||
|
||||
// Verificar status novamente após lock
|
||||
if (!"GENERATED".equals(payrollRun.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Apenas execuções GENERATED podem ser processadas",
|
||||
"INVALID_RUN_STATUS",
|
||||
HttpStatus.PRECONDITION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Atualizar status para PROCESSING (com lock)
|
||||
payrollRun.setStatus("PROCESSING");
|
||||
payrollRunRepository.save(payrollRun);
|
||||
|
||||
try {
|
||||
// ... resto do processamento
|
||||
} catch (Exception e) {
|
||||
payrollRun.setStatus("FAILED");
|
||||
payrollRunRepository.save(payrollRun);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ou usar `@Version` para optimistic locking:**
|
||||
```java
|
||||
@Entity
|
||||
public class PayrollRun extends AuditableEntity {
|
||||
@Version
|
||||
private Long version; // Para optimistic locking
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 14: Falta controle de concorrência em geração de itens**
|
||||
|
||||
**Problema:** Múltiplos usuários podem gerar itens para a mesma folha simultaneamente.
|
||||
|
||||
**Solução:** Similar ao problema 13, adicionar lock ou validação de status.
|
||||
|
||||
---
|
||||
|
||||
## 5. Análise de Validações de Conformidade Legal
|
||||
|
||||
### 5.1 Decreto 12-A/94
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 15: Validação de promoção incompleta**
|
||||
|
||||
Já detalhado na seção 1.2 (Problema 4).
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 16: Falta validação de idade mínima**
|
||||
|
||||
**Localização:** `AgentService.create()`
|
||||
|
||||
**Problema:** Não valida idade mínima de 18 anos para admissão.
|
||||
|
||||
**Solução:**
|
||||
```java
|
||||
if (dto.getBirthDate() != null && dto.getHireDate() != null) {
|
||||
long age = ChronoUnit.YEARS.between(dto.getBirthDate(), dto.getHireDate());
|
||||
if (age < 18) {
|
||||
throw new BusinessException(
|
||||
"Idade mínima para admissão: 18 anos",
|
||||
"MINIMUM_AGE_NOT_MET",
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 17: Falta validação de habilitação literária**
|
||||
|
||||
**Problema:** Não valida se o agente tem habilitação literária adequada para a categoria.
|
||||
|
||||
**Solução:** Adicionar campo `requiredQualification` em `SalaryCategory` e validar em `AgentService.update()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Análise de Performance e Otimizações
|
||||
|
||||
### 6.1 Problemas de Performance
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 18: N+1 queries em geração de folha**
|
||||
|
||||
**Localização:** `PayrollService.generatePayrollItems()`
|
||||
|
||||
**Problema:**
|
||||
```java
|
||||
for (Agent agent : activeAgents) {
|
||||
SalaryGrid salary = salaryGridRepository.findByStepIdAndDate(
|
||||
agent.getSalaryStep(), LocalDate.now()); // Query por agente
|
||||
|
||||
// Múltiplas queries dentro do loop
|
||||
List<AttendanceRecord> attendanceRecords = attendanceRecordRepository
|
||||
.findByAgentIdAndDateRange(...); // Query por agente
|
||||
|
||||
List<Absence> legacyAbsences = absenceRepository
|
||||
.findByAgentIdAndDateRange(...); // Query por agente
|
||||
}
|
||||
```
|
||||
|
||||
**Solução:** Usar batch queries ou `@EntityGraph`:
|
||||
```java
|
||||
// Buscar todos os SalaryGrids de uma vez
|
||||
Map<UUID, SalaryGrid> salaryGrids = salaryGridRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.filter(g -> g.getValidFrom().isBefore(LocalDate.now()) &&
|
||||
(g.getValidTo() == null || g.getValidTo().isAfter(LocalDate.now())))
|
||||
.collect(Collectors.toMap(
|
||||
g -> g.getStep().getId(),
|
||||
g -> g,
|
||||
(existing, replacement) -> existing // Em caso de duplicata, manter o primeiro
|
||||
));
|
||||
|
||||
// Buscar todas as ausências de uma vez
|
||||
List<UUID> agentIds = activeAgents.stream()
|
||||
.map(Agent::getId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<UUID, List<Absence>> absencesByAgent = absenceRepository
|
||||
.findByAgentIdInAndDateRange(agentIds, periodStart, periodEnd)
|
||||
.stream()
|
||||
.collect(Collectors.groupingBy(a -> a.getAgent().getId()));
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 19: Falta índice para queries frequentes**
|
||||
|
||||
**Queries que precisam de índices:**
|
||||
1. `PayrollRun` por período + ministério + orgUnit
|
||||
2. `Absence` por agente + período
|
||||
3. `AttendanceRecord` por agente + período
|
||||
4. `PerformanceEvaluation` por agente + ano
|
||||
|
||||
**Solução SQL:**
|
||||
```sql
|
||||
-- Já existem alguns índices, mas faltam:
|
||||
CREATE INDEX idx_payroll_run_period_ministry_org
|
||||
ON payroll_run(period_id, ministry_id, org_unit_id, status);
|
||||
|
||||
CREATE INDEX idx_absence_agent_date_range
|
||||
ON absence(agent_id, start_date, end_date);
|
||||
|
||||
CREATE INDEX idx_attendance_agent_date_range
|
||||
ON attendance_record(agent_id, date);
|
||||
|
||||
CREATE INDEX idx_evaluation_agent_year
|
||||
ON performance_evaluations(agent_id, reference_year);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Análise de Tratamento de Erros
|
||||
|
||||
### 7.1 Inconsistências no Tratamento de Erros
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 20: Uso inconsistente de exceções**
|
||||
|
||||
**Problema:** Mistura de `IllegalArgumentException`, `BusinessException`, `ResourceNotFoundException`.
|
||||
|
||||
**Exemplos:**
|
||||
```java
|
||||
// AgentService
|
||||
.orElseThrow(() -> new IllegalArgumentException("Agente não encontrado"));
|
||||
|
||||
// PayrollService
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Período não encontrado"));
|
||||
|
||||
// PayrollController
|
||||
catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
```
|
||||
|
||||
**Solução:** Padronizar uso:
|
||||
- `ResourceNotFoundException` → 404 Not Found
|
||||
- `BusinessException` → 400 Bad Request ou 422 Unprocessable Entity
|
||||
- `IllegalArgumentException` → Apenas para validações de parâmetros (não para recursos não encontrados)
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 21: Falta logging adequado**
|
||||
|
||||
**Problema:** Alguns erros não são logados, dificultando debugging.
|
||||
|
||||
**Solução:** Adicionar logging em todos os catch blocks:
|
||||
```java
|
||||
catch (BusinessException e) {
|
||||
log.warn("Erro de negócio ao processar folha {}: {}", payrollRunId, e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao processar folha {}: {}", payrollRunId, e.getMessage(), e);
|
||||
throw new BusinessException("Erro ao processar folha", "PROCESSING_ERROR", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Análise de Segurança
|
||||
|
||||
### 8.1 Problemas de Segurança
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 22: Falta validação de permissões**
|
||||
|
||||
**Problema:** Não há validação de se o usuário tem permissão para processar folha de determinado ministério/orgUnit.
|
||||
|
||||
**Solução:** Adicionar validação baseada em roles/permissões:
|
||||
```java
|
||||
@PreAuthorize("hasRole('PAYROLL_PROCESSOR') and " +
|
||||
"@securityService.canProcessPayrollForOrgUnit(#orgUnitId)")
|
||||
public void processPayrollRun(UUID payrollRunId) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 23: Falta auditoria de mudanças críticas**
|
||||
|
||||
**Problema:** Mudanças em salários, categorias, etc. não são auditadas adequadamente.
|
||||
|
||||
**Solução:** Usar `@EntityListeners` ou Spring Data JPA Auditing para rastrear todas as mudanças.
|
||||
|
||||
---
|
||||
|
||||
## 9. Análise de Frontend
|
||||
|
||||
### 9.1 Problemas Identificados
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 24: Falta validação de formulários no frontend**
|
||||
|
||||
**Problema:** Alguns formulários não validam dados antes de enviar.
|
||||
|
||||
**Solução:** Adicionar validação com `zod` ou `yup` em todos os formulários.
|
||||
|
||||
#### ⚠️ **PROBLEMA MÉDIO 25: Falta tratamento de erros específicos**
|
||||
|
||||
**Problema:** Erros genéricos não mostram mensagens específicas ao usuário.
|
||||
|
||||
**Solução:** Mapear códigos de erro do backend para mensagens amigáveis.
|
||||
|
||||
---
|
||||
|
||||
## 10. Análise de Testes
|
||||
|
||||
### 10.1 Cobertura de Testes
|
||||
|
||||
#### ❌ **PROBLEMA CRÍTICO 26: Cobertura de testes insuficiente**
|
||||
|
||||
**Problema:** Apenas `PayrollServiceTest` existe, com poucos cenários.
|
||||
|
||||
**Faltam testes para:**
|
||||
1. Validações de negócio
|
||||
2. Edge cases (cálculo proporcional, faltas, etc.)
|
||||
3. Integrações (Orçamento, Tesouro)
|
||||
4. Concorrência
|
||||
5. Validações de conformidade legal
|
||||
|
||||
**Solução:** Criar suite completa de testes unitários e de integração.
|
||||
|
||||
---
|
||||
|
||||
## 11. Problemas Críticos Identificados
|
||||
|
||||
### Prioridade CRÍTICA (Resolver Imediatamente)
|
||||
|
||||
1. **P1 - Falta validação de duplicidade de PayrollRun** (Problema 1)
|
||||
2. **P2 - Falta validação de agentes elegíveis para folha** (Problema 2)
|
||||
3. **P3 - Falta cálculo proporcional de salário** (Problema 7)
|
||||
4. **P4 - Falta controle de concorrência em PayrollRun** (Problema 13)
|
||||
5. **P5 - Validação de promoção incompleta** (Problema 4)
|
||||
6. **P6 - Falta constraint UNIQUE para PayrollRun** (Problema 10)
|
||||
|
||||
### Prioridade ALTA (Resolver em Breve)
|
||||
|
||||
7. **P7 - Falta validação de período fechado** (Problema 3)
|
||||
8. **P8 - Falta validação de sobreposição de contratos** (Problema 5)
|
||||
9. **P9 - Cálculo de faltas assume 30 dias fixos** (Problema 8)
|
||||
10. **P10 - Falta sincronização de deductedInPayrollRunId** (Problema 12)
|
||||
11. **P11 - N+1 queries em geração de folha** (Problema 18)
|
||||
12. **P12 - Cobertura de testes insuficiente** (Problema 26)
|
||||
|
||||
### Prioridade MÉDIA (Melhorias Desejáveis)
|
||||
|
||||
13. **P13 - Falta validação de sobreposição de regras globais** (Problema 6)
|
||||
14. **P14 - Falta validação de sobreposição de escalões de imposto** (Problema 9)
|
||||
15. **P15 - Falta constraint CHECK para datas** (Problema 11)
|
||||
16. **P16 - Falta validação de idade mínima** (Problema 16)
|
||||
17. **P17 - Falta validação de habilitação literária** (Problema 17)
|
||||
18. **P18 - Uso inconsistente de exceções** (Problema 20)
|
||||
19. **P19 - Falta logging adequado** (Problema 21)
|
||||
20. **P20 - Falta validação de permissões** (Problema 22)
|
||||
|
||||
---
|
||||
|
||||
## 12. Recomendações Prioritárias
|
||||
|
||||
### Fase 1: Correções Críticas (1-2 semanas)
|
||||
|
||||
1. Implementar validação de duplicidade de PayrollRun
|
||||
2. Implementar validação completa de elegibilidade de agentes
|
||||
3. Implementar cálculo proporcional de salário
|
||||
4. Adicionar controle de concorrência (locks)
|
||||
5. Completar validação de promoções
|
||||
6. Adicionar constraints de banco de dados
|
||||
|
||||
### Fase 2: Melhorias Importantes (2-4 semanas)
|
||||
|
||||
7. Corrigir cálculo de faltas (dias do mês específico)
|
||||
8. Implementar validações de sobreposição
|
||||
9. Otimizar queries (eliminar N+1)
|
||||
10. Adicionar sincronização de `deductedInPayrollRunId`
|
||||
11. Melhorar tratamento de erros e logging
|
||||
12. Adicionar testes unitários e de integração
|
||||
|
||||
### Fase 3: Melhorias e Refinamentos (1-2 meses)
|
||||
|
||||
13. Adicionar validações de conformidade legal completas
|
||||
14. Implementar auditoria completa
|
||||
15. Melhorar validações de frontend
|
||||
16. Adicionar índices de performance
|
||||
17. Implementar validação de permissões
|
||||
18. Documentação completa de regras de negócio
|
||||
|
||||
---
|
||||
|
||||
## 📊 Resumo Executivo
|
||||
|
||||
### Estatísticas
|
||||
|
||||
- **Problemas Críticos:** 6
|
||||
- **Problemas de Alta Prioridade:** 6
|
||||
- **Problemas de Média Prioridade:** 8
|
||||
- **Total de Problemas Identificados:** 20
|
||||
|
||||
### Impacto Estimado
|
||||
|
||||
- **Risco Financeiro:** ALTO (duplicação de pagamentos, cálculos incorretos)
|
||||
- **Risco Legal:** MÉDIO (não conformidade com Decreto 12-A/94)
|
||||
- **Risco Operacional:** ALTO (race conditions, dados inconsistentes)
|
||||
- **Risco de Performance:** MÉDIO (N+1 queries, falta de índices)
|
||||
|
||||
### Conclusão
|
||||
|
||||
O módulo RH & Folha apresenta uma **arquitetura sólida** e **lógica de negócio bem estruturada**, mas possui **lacunas significativas** em validações, tratamento de edge cases, controle de concorrência e conformidade legal.
|
||||
|
||||
As correções críticas devem ser implementadas **imediatamente** para evitar problemas em produção, especialmente relacionados a:
|
||||
- Duplicação de execuções de folha
|
||||
- Cálculos incorretos de salários proporcionais
|
||||
- Race conditions em processamento simultâneo
|
||||
- Falta de validações de elegibilidade
|
||||
|
||||
---
|
||||
|
||||
**Análise realizada por:** Cursor AI
|
||||
**Data:** 2025-01-27
|
||||
**Versão:** 2.0 (Ultra Profunda)
|
||||
**Metodologia:** Análise estática de código, revisão de lógica de negócio, identificação de edge cases, análise de integridade de dados, análise de concorrência
|
||||
|
||||
Reference in New Issue
Block a user