33 KiB
🔬 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
- Análise de Validações de Negócio
- Análise de Edge Cases e Boundary Conditions
- Análise de Integridade de Dados
- Análise de Concorrência e Race Conditions
- Análise de Validações de Conformidade Legal
- Análise de Performance e Otimizações
- Análise de Tratamento de Erros
- Análise de Segurança
- Análise de Frontend
- Análise de Testes
- Problemas Críticos Identificados
- 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:
// 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:
// 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:
// 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:
- Contrato ativo: Não verifica se o agente tem
AgentContractativo - Data de admissão: Não verifica se o agente foi admitido antes do período da folha
- Data de término: Não verifica se o agente foi desligado durante o período
- Período probatório: Não considera se está em período probatório (pode ter regras diferentes)
- Suspensão: Não verifica se o agente está suspenso durante o período
Solução:
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:
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:
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:
- 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
- Tempo mínimo na categoria: Para promoção (mudança de categoria), exige tempo mínimo na categoria atual
- Habilitação literária: Verificar se a nova categoria requer habilitação literária superior
- Vagas disponíveis: Verificar se há vaga na categoria/grau de destino
- Status da avaliação: Verificar se as avaliações estão com status "FINAL" (não apenas "DRAFT")
Solução:
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:
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:
public GlobalDeductionRule saveRule(GlobalDeductionRule rule) {
// Futuro: Adicionar validações de sobreposição de datas
return globalDeductionRuleRepository.save(rule);
}
Solução:
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:
- Agente admitido durante o período → Deve receber proporcional
- Agente desligado durante o período → Deve receber proporcional
- Agente suspenso durante o período → Deve ter desconto proporcional
- Agente em período probatório → Pode ter regras diferentes
Solução:
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:
// 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:
- Fallback de 30 dias é incorreto (fevereiro tem 28/29, meses têm 30/31)
- Não considera dias úteis vs. dias corridos
- Não considera feriados
Solução:
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:
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:
-- 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:
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:
-- 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:
// 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:
@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:
@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:
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:
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:
// 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:
PayrollRunpor período + ministério + orgUnitAbsencepor agente + períodoAttendanceRecordpor agente + períodoPerformanceEvaluationpor agente + ano
Solução 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:
// 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 FoundBusinessException→ 400 Bad Request ou 422 Unprocessable EntityIllegalArgumentException→ 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:
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:
@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:
- Validações de negócio
- Edge cases (cálculo proporcional, faltas, etc.)
- Integrações (Orçamento, Tesouro)
- Concorrência
- 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)
- P1 - Falta validação de duplicidade de PayrollRun (Problema 1)
- P2 - Falta validação de agentes elegíveis para folha (Problema 2)
- P3 - Falta cálculo proporcional de salário (Problema 7)
- P4 - Falta controle de concorrência em PayrollRun (Problema 13)
- P5 - Validação de promoção incompleta (Problema 4)
- P6 - Falta constraint UNIQUE para PayrollRun (Problema 10)
Prioridade ALTA (Resolver em Breve)
- P7 - Falta validação de período fechado (Problema 3)
- P8 - Falta validação de sobreposição de contratos (Problema 5)
- P9 - Cálculo de faltas assume 30 dias fixos (Problema 8)
- P10 - Falta sincronização de deductedInPayrollRunId (Problema 12)
- P11 - N+1 queries em geração de folha (Problema 18)
- P12 - Cobertura de testes insuficiente (Problema 26)
Prioridade MÉDIA (Melhorias Desejáveis)
- P13 - Falta validação de sobreposição de regras globais (Problema 6)
- P14 - Falta validação de sobreposição de escalões de imposto (Problema 9)
- P15 - Falta constraint CHECK para datas (Problema 11)
- P16 - Falta validação de idade mínima (Problema 16)
- P17 - Falta validação de habilitação literária (Problema 17)
- P18 - Uso inconsistente de exceções (Problema 20)
- P19 - Falta logging adequado (Problema 21)
- P20 - Falta validação de permissões (Problema 22)
12. Recomendações Prioritárias
Fase 1: Correções Críticas (1-2 semanas)
- Implementar validação de duplicidade de PayrollRun
- Implementar validação completa de elegibilidade de agentes
- Implementar cálculo proporcional de salário
- Adicionar controle de concorrência (locks)
- Completar validação de promoções
- Adicionar constraints de banco de dados
Fase 2: Melhorias Importantes (2-4 semanas)
- Corrigir cálculo de faltas (dias do mês específico)
- Implementar validações de sobreposição
- Otimizar queries (eliminar N+1)
- Adicionar sincronização de
deductedInPayrollRunId - Melhorar tratamento de erros e logging
- Adicionar testes unitários e de integração
Fase 3: Melhorias e Refinamentos (1-2 meses)
- Adicionar validações de conformidade legal completas
- Implementar auditoria completa
- Melhorar validações de frontend
- Adicionar índices de performance
- Implementar validação de permissões
- 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