987 lines
33 KiB
Markdown
987 lines
33 KiB
Markdown
|
|
# 🔬 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
|
||
|
|
|