Files
sigrhapf/Documents/sigfip/sigefp/ANALISE_ULTRA_PROFUNDA_MODULO_RH_FOLHA.md
2026-05-19 11:45:46 +00:00

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

  1. Análise de Validações de Negócio
  2. Análise de Edge Cases e Boundary Conditions
  3. Análise de Integridade de Dados
  4. Análise de Concorrência e Race Conditions
  5. Análise de Validações de Conformidade Legal
  6. Análise de Performance e Otimizações
  7. Análise de Tratamento de Erros
  8. Análise de Segurança
  9. Análise de Frontend
  10. Análise de Testes
  11. Problemas Críticos Identificados
  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:

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

  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:

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:

  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:

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:

  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:

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:

  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:

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

  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:

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

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:

  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)

  1. P7 - Falta validação de período fechado (Problema 3)
  2. P8 - Falta validação de sobreposição de contratos (Problema 5)
  3. P9 - Cálculo de faltas assume 30 dias fixos (Problema 8)
  4. P10 - Falta sincronização de deductedInPayrollRunId (Problema 12)
  5. P11 - N+1 queries em geração de folha (Problema 18)
  6. P12 - Cobertura de testes insuficiente (Problema 26)

Prioridade MÉDIA (Melhorias Desejáveis)

  1. P13 - Falta validação de sobreposição de regras globais (Problema 6)
  2. P14 - Falta validação de sobreposição de escalões de imposto (Problema 9)
  3. P15 - Falta constraint CHECK para datas (Problema 11)
  4. P16 - Falta validação de idade mínima (Problema 16)
  5. P17 - Falta validação de habilitação literária (Problema 17)
  6. P18 - Uso inconsistente de exceções (Problema 20)
  7. P19 - Falta logging adequado (Problema 21)
  8. 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)

  1. Corrigir cálculo de faltas (dias do mês específico)
  2. Implementar validações de sobreposição
  3. Otimizar queries (eliminar N+1)
  4. Adicionar sincronização de deductedInPayrollRunId
  5. Melhorar tratamento de erros e logging
  6. Adicionar testes unitários e de integração

Fase 3: Melhorias e Refinamentos (1-2 meses)

  1. Adicionar validações de conformidade legal completas
  2. Implementar auditoria completa
  3. Melhorar validações de frontend
  4. Adicionar índices de performance
  5. Implementar validação de permissões
  6. 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