# 🔬 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 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 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 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 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 events = careerEventRepository .findByAgentIdOrderByEffectiveDateDesc(agent.getId()); Optional 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 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 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 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 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 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 attendanceRecords = attendanceRecordRepository .findByAgentIdAndDateRange(...); // Query por agente List legacyAbsences = absenceRepository .findByAgentIdAndDateRange(...); // Query por agente } ``` **Solução:** Usar batch queries ou `@EntityGraph`: ```java // Buscar todos os SalaryGrids de uma vez Map 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 agentIds = activeAgents.stream() .map(Agent::getId) .collect(Collectors.toList()); Map> 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