feat: otimização de performance e ajustes finais

This commit is contained in:
Idrissa Banora
2026-05-18 10:49:32 +00:00
commit 430deed1cd
530 changed files with 150759 additions and 0 deletions
@@ -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