feat: otimização de performance e ajustes finais

This commit is contained in:
Idrissa Banora
2026-05-18 10:49:32 +00:00
commit 52a7c4f9cf
579 changed files with 156489 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>br.gov.sigefp</groupId>
<artifactId>sigefp-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>sigefp-rh</artifactId>
<packaging>jar</packaging>
<name>SIGEFP RH</name>
<description>Módulo de recursos humanos: agentes, contratos, folha de pagamento</description>
<dependencies>
<dependency>
<groupId>br.gov.sigefp</groupId>
<artifactId>sigefp-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>br.gov.sigefp</groupId>
<artifactId>sigefp-org</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>br.gov.sigefp</groupId>
<artifactId>sigefp-budget</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,53 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.AbsenceDTO;
import br.gov.sigefp.rh.api.dto.CreateAbsenceDTO;
import br.gov.sigefp.rh.service.AbsenceService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/api/rh/absences")
@RequiredArgsConstructor
public class AbsenceController {
private final AbsenceService absenceService;
@PostMapping
public ResponseEntity<AbsenceDTO> create(@Valid @RequestBody CreateAbsenceDTO dto) {
AbsenceDTO created = absenceService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/agent/{agentId}")
public ResponseEntity<Page<AbsenceDTO>> findByAgent(
@PathVariable UUID agentId,
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sortBy", required = false) String sortBy,
@RequestParam(name = "sortDirection", required = false, defaultValue = "DESC") String sortDirection) {
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.DESC, "startDate");
Pageable pageable = PageRequest.of(page, size, sort);
Page<AbsenceDTO> result = absenceService.findByAgent(agentId, pageable);
return ResponseEntity.ok(result);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
absenceService.delete(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,46 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.domain.AgentBankAccount;
import br.gov.sigefp.rh.repository.AgentBankAccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/rh/bank-accounts")
@RequiredArgsConstructor
public class AgentBankAccountController {
private final AgentBankAccountRepository repository;
private final br.gov.sigefp.rh.service.AgentBankAccountService service;
@GetMapping
public List<AgentBankAccount> findAll() {
return repository.findAll();
}
@GetMapping("/agent/{agentId}")
public List<AgentBankAccount> findByAgent(@PathVariable UUID agentId) {
return repository.findByAgentId(agentId);
}
@PostMapping
public AgentBankAccount save(@RequestBody AgentBankAccount account) {
return service.save(account);
}
@PutMapping("/{id}")
public AgentBankAccount update(@PathVariable("id") UUID id, @RequestBody AgentBankAccount account) {
account.setId(id);
return service.update(account);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") UUID id) {
repository.deleteById(id);
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,46 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.domain.AgentContract;
import br.gov.sigefp.rh.repository.AgentContractRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/rh/contracts")
@RequiredArgsConstructor
public class AgentContractController {
private final AgentContractRepository repository;
private final br.gov.sigefp.rh.service.AgentContractService contractService;
@GetMapping
public List<AgentContract> findAll() {
return repository.findAll();
}
@GetMapping("/agent/{agentId}")
public List<AgentContract> findByAgent(@PathVariable("agentId") UUID agentId) {
return repository.findByAgentId(agentId);
}
@PostMapping
public AgentContract save(@RequestBody AgentContract contract) {
return contractService.saveContract(contract);
}
@PutMapping("/{id}")
public AgentContract update(@PathVariable("id") UUID id, @RequestBody AgentContract contract) {
contract.setId(id);
return contractService.updateContract(contract);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") UUID id) {
repository.deleteById(id);
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,106 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.*;
import br.gov.sigefp.rh.service.AgentService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/**
* Controller REST para gestão de agentes.
*/
@RestController
@RequestMapping("/api/rh/agents")
@RequiredArgsConstructor
public class AgentController {
private final AgentService agentService;
@PostMapping
public ResponseEntity<AgentDTO> create(@Valid @RequestBody AgentDTO dto) {
AgentDTO created = agentService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<AgentDTO> update(
@PathVariable("id") UUID id,
@Valid @RequestBody AgentDTO dto) {
AgentDTO updated = agentService.update(id, dto);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") UUID id) {
agentService.delete(id);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}")
public ResponseEntity<AgentDTO> findById(@PathVariable("id") UUID id) {
AgentDTO dto = agentService.findById(id);
return ResponseEntity.ok(dto);
}
@GetMapping
public ResponseEntity<Page<AgentDTO>> findAll(
@RequestParam(value = "query", required = false) String query,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "ministry", required = false) UUID ministry,
@RequestParam(value = "orgUnit", required = false) UUID orgUnit,
@RequestParam(value = "position", required = false) UUID position,
@RequestParam(value = "functionalSituation", required = false) String functionalSituation,
@RequestParam(value = "appointmentType", required = false) String appointmentType,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.ASC, "createdAt");
Pageable pageable = PageRequest.of(page, size, sort);
Page<AgentDTO> result = agentService.findAllWithFilters(
query, status, ministry, orgUnit, position, functionalSituation, appointmentType, pageable);
return ResponseEntity.ok(result);
}
@GetMapping("/stats")
public ResponseEntity<AgentStatsDTO> getStats(
@RequestParam(value = "query", required = false) String query,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "ministry", required = false) UUID ministry,
@RequestParam(value = "orgUnit", required = false) UUID orgUnit,
@RequestParam(value = "position", required = false) UUID position,
@RequestParam(value = "functionalSituation", required = false) String functionalSituation,
@RequestParam(value = "appointmentType", required = false) String appointmentType) {
System.err.println("************************************************************");
System.err.println("API STATS REQUEST RECEIVED");
System.err.println("Ministry: " + ministry);
System.err.println("OrgUnit: " + orgUnit);
System.err.println("Query: " + query);
System.err.println("************************************************************");
return ResponseEntity.ok(agentService.getStats(query, status, ministry, orgUnit, position, functionalSituation,
appointmentType));
}
@GetMapping("/{id}/history")
public ResponseEntity<List<CareerTimelineDTO>> getHistory(@PathVariable("id") UUID id) {
return ResponseEntity.ok(agentService.getTimeline(id));
}
@GetMapping("/{id}/status-history")
public ResponseEntity<List<AgentStatusHistoryDTO>> getStatusHistory(@PathVariable("id") UUID id) {
return ResponseEntity.ok(agentService.getStatusHistory(id));
}
}
@@ -0,0 +1,82 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.AttendanceRecordDTO;
import br.gov.sigefp.rh.api.dto.MonthlyAttendanceSheetDTO;
import br.gov.sigefp.rh.domain.AttendanceRecord;
import br.gov.sigefp.rh.domain.MonthlyAttendanceSheet;
import br.gov.sigefp.rh.repository.AttendanceRecordRepository;
import br.gov.sigefp.rh.service.AttendanceService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/rh/attendance")
@RequiredArgsConstructor
public class AttendanceController {
private final AttendanceService attendanceService;
private final AttendanceRecordRepository recordRepository;
@GetMapping("/sheet")
public ResponseEntity<MonthlyAttendanceSheetDTO> getSheet(
@RequestParam UUID orgUnitId,
@RequestParam int month,
@RequestParam int year) {
MonthlyAttendanceSheet sheet = attendanceService.getOrCreateSheet(orgUnitId, month, year);
return ResponseEntity.ok(toSummaryDTO(sheet));
}
@GetMapping("/sheet/{sheetId}/records")
public ResponseEntity<List<AttendanceRecordDTO>> getRecords(@PathVariable UUID sheetId) {
List<AttendanceRecord> records = recordRepository.findBySheetId(sheetId);
return ResponseEntity.ok(records.stream().map(this::toRecordDTO).collect(Collectors.toList()));
}
@PostMapping("/sheet/{sheetId}/approve")
public ResponseEntity<Void> approveSheet(@PathVariable UUID sheetId) {
// TODO: Extract user from Security Context
attendanceService.approveSheet(sheetId, "Admin User");
return ResponseEntity.ok().build();
}
@PostMapping("/sheet/{sheetId}/reopen")
public ResponseEntity<Void> reopenSheet(@PathVariable UUID sheetId) {
attendanceService.reopenSheet(sheetId);
return ResponseEntity.ok().build();
}
@PostMapping("/sheet/{sheetId}/upload")
public ResponseEntity<Void> uploadExcel(@PathVariable UUID sheetId, @RequestParam("file") MultipartFile file) {
attendanceService.importExcel(sheetId, file);
return ResponseEntity.ok().build();
}
private MonthlyAttendanceSheetDTO toSummaryDTO(MonthlyAttendanceSheet sheet) {
return MonthlyAttendanceSheetDTO.builder()
.id(sheet.getId())
.orgUnitId(sheet.getOrgUnitId())
.month(sheet.getMonth())
.year(sheet.getYear())
.status(sheet.getStatus())
.approvedBy(sheet.getApprovedBy())
.build();
}
private AttendanceRecordDTO toRecordDTO(AttendanceRecord record) {
return AttendanceRecordDTO.builder()
.id(record.getId())
.agentId(record.getAgent().getId())
.agentName(record.getAgent().getFullName())
.date(record.getDate())
.type(record.getType())
.observation(record.getObservation())
.build();
}
}
@@ -0,0 +1,36 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.CareerRegimeDTO;
import br.gov.sigefp.rh.service.CareerRegimeService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/rh/career-regimes")
@RequiredArgsConstructor
public class CareerRegimeController {
private final CareerRegimeService service;
@GetMapping
public List<CareerRegimeDTO> findAll() {
return service.findAll();
}
@PostMapping
public CareerRegimeDTO create(@RequestBody CareerRegimeDTO dto) {
return service.create(dto);
}
@PutMapping("/{id}")
public CareerRegimeDTO update(@PathVariable java.util.UUID id, @RequestBody CareerRegimeDTO dto) {
return service.update(id, dto);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable java.util.UUID id) {
service.delete(id);
}
}
@@ -0,0 +1,125 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.AgentImportResultDTO;
import br.gov.sigefp.rh.service.AgentImportService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/rh/agents/imports")
@RequiredArgsConstructor
@Slf4j
public class ImportController {
private final AgentImportService importService;
@PostMapping("/excel")
@PreAuthorize("hasRole('HR_ADMIN')")
public ResponseEntity<AgentImportResultDTO> importAgents(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResponseEntity.badRequest().body(
AgentImportResultDTO.builder()
.errors(java.util.List.of("Arquivo está vazio."))
.build());
}
log.info("Iniciando importação de agentes a partir do arquivo: {}", file.getOriginalFilename());
AgentImportResultDTO result = importService.importAgents(file);
return ResponseEntity.ok(result);
}
@GetMapping("/template")
public ResponseEntity<byte[]> getTemplate() throws java.io.IOException {
try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
org.apache.poi.ss.usermodel.Sheet sheet = workbook.createSheet("Agentes");
// Header style
org.apache.poi.ss.usermodel.CellStyle headerStyle = workbook.createCellStyle();
org.apache.poi.ss.usermodel.Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
// Date cell style (formato texto para evitar conversão automática do Excel)
org.apache.poi.ss.usermodel.CellStyle textStyle = workbook.createCellStyle();
org.apache.poi.ss.usermodel.DataFormat textFormat = workbook.createDataFormat();
textStyle.setDataFormat(textFormat.getFormat("@")); // Formato texto
org.apache.poi.ss.usermodel.Row header = sheet.createRow(0);
String[] columns = {
"NOME", "NIF", "MATRICULA", "BI", "DATA_NASCIMENTO", "SEXO", "ESTADO_CIVIL",
"HABILITACAO_LITERARIA", "CATEGORIA_CODIGO", "UNIDADE_ORG_CODIGO",
"NOMEACAO", "TIPO_CONTRATO", "DATA_INICIO_CONTRATO", "DATA_FIM_CONTRATO",
"BANCO_CODIGO", "IBAN"
};
// Colunas de data: 4 (DATA_NASCIMENTO), 10 (NOMEACAO), 12 (DATA_INICIO), 13
// (DATA_FIM)
int[] dateColumns = { 4, 10, 12, 13 };
for (int i = 0; i < columns.length; i++) {
org.apache.poi.ss.usermodel.Cell cell = header.createCell(i);
cell.setCellValue(columns[i]);
cell.setCellStyle(headerStyle);
sheet.setColumnWidth(i, 4500); // Largura padrão
}
// Formatar colunas de data como texto para toda a coluna
for (int dateCol : dateColumns) {
sheet.setDefaultColumnStyle(dateCol, textStyle);
}
// Sample Row
org.apache.poi.ss.usermodel.Row sample = sheet.createRow(1);
sample.createCell(0).setCellValue("Exemplo João Silva");
sample.createCell(1).setCellValue("999999999");
sample.createCell(2).setCellValue("MAT-001");
sample.createCell(3).setCellValue("12345678");
// Datas como texto para evitar problemas de formatação
org.apache.poi.ss.usermodel.Cell dataNasc = sample.createCell(4);
dataNasc.setCellValue("01/01/1980");
dataNasc.setCellStyle(textStyle);
sample.createCell(5).setCellValue("M");
sample.createCell(6).setCellValue("SOLTEIRO");
sample.createCell(7).setCellValue("LICENCIATURA");
sample.createCell(8).setCellValue("");
sample.createCell(9).setCellValue("");
org.apache.poi.ss.usermodel.Cell nomeacao = sample.createCell(10);
nomeacao.setCellValue("15/01/2024");
nomeacao.setCellStyle(textStyle);
sample.createCell(11).setCellValue("NOMEACAO");
org.apache.poi.ss.usermodel.Cell dataInicio = sample.createCell(12);
dataInicio.setCellValue("01/01/2024");
dataInicio.setCellStyle(textStyle);
org.apache.poi.ss.usermodel.Cell dataFim = sample.createCell(13);
dataFim.setCellValue("");
dataFim.setCellStyle(textStyle);
sample.createCell(14).setCellValue("");
sample.createCell(15).setCellValue("");
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
workbook.write(out);
return ResponseEntity.ok()
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=template_agentes.xlsx")
.contentType(org.springframework.http.MediaType
.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.body(out.toByteArray());
}
}
}
@@ -0,0 +1,135 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.rh.api.dto.CreatePayrollPeriodDTO;
import br.gov.sigefp.rh.api.dto.CreatePayrollRunDTO;
import br.gov.sigefp.rh.api.dto.PayrollPeriodDTO;
import br.gov.sigefp.rh.api.dto.PayrollRunDTO;
import br.gov.sigefp.rh.service.PayrollService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
/**
* Controller REST para gestão de folha de pagamento.
*/
@RestController
@RequestMapping("/api/rh")
@RequiredArgsConstructor
public class PayrollController {
private final PayrollService payrollService;
@GetMapping("/payroll-periods")
public ResponseEntity<List<PayrollPeriodDTO>> findAllPeriods() {
List<PayrollPeriodDTO> periods = payrollService.findAllPeriods();
return ResponseEntity.ok(periods);
}
@PostMapping("/payroll-periods")
public ResponseEntity<PayrollPeriodDTO> createPeriod(@Valid @RequestBody CreatePayrollPeriodDTO dto) {
try {
PayrollPeriodDTO created = payrollService.createPeriod(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/payroll-periods/{id}/status")
public ResponseEntity<PayrollPeriodDTO> updatePeriodStatus(
@PathVariable UUID id,
@RequestParam String status) {
try {
PayrollPeriodDTO updated = payrollService.updatePeriodStatus(id, status);
return ResponseEntity.ok(updated);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/payroll-runs")
public ResponseEntity<List<PayrollRunDTO>> findAllPayrollRuns() {
List<PayrollRunDTO> runs = payrollService.findAllPayrollRuns();
return ResponseEntity.ok(runs);
}
@PostMapping("/payroll-runs")
public ResponseEntity<PayrollRunDTO> createPayrollRun(@Valid @RequestBody CreatePayrollRunDTO dto) {
try {
PayrollRunDTO created = payrollService.createPayrollRun(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@GetMapping("/payroll-runs/{id}")
public ResponseEntity<PayrollRunDTO> findPayrollRunById(@PathVariable("id") UUID id) {
try {
PayrollRunDTO dto = payrollService.findPayrollRunById(id);
return ResponseEntity.ok(dto);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
/**
* Processa uma execução de folha, criando execuções orçamentárias.
* POST /api/rh/payroll-runs/{id}/process
*/
@PostMapping("/payroll-runs/{id}/process")
public ResponseEntity<Void> processPayrollRun(@PathVariable("id") UUID id) {
payrollService.processPayrollRun(id);
return ResponseEntity.ok().build();
}
/**
* Gera automaticamente os itens de folha para a execução.
* POST /api/rh/payroll-runs/{id}/generate
*/
@PostMapping("/payroll-runs/{id}/generate")
public ResponseEntity<Void> generatePayrollItems(@PathVariable("id") UUID id) {
payrollService.generatePayrollItems(id);
return ResponseEntity.ok().build();
}
/**
* Encerra definitivamente uma execução de folha.
* POST /api/rh/payroll-runs/{id}/close
*/
@PostMapping("/payroll-runs/{id}/close")
public ResponseEntity<Void> closePayrollRun(@PathVariable("id") UUID id) {
payrollService.closePayrollRun(id);
return ResponseEntity.ok().build();
}
/**
* Exclui uma execução de folha (apenas se status PENDING ou GENERATED).
* DELETE /api/rh/payroll-runs/{id}
*/
@DeleteMapping("/payroll-runs/{id}")
public ResponseEntity<Void> deletePayrollRun(@PathVariable("id") UUID id) {
payrollService.deletePayrollRun(id);
return ResponseEntity.noContent().build();
}
/**
* Diagnóstico: Verifica todos os requisitos para geração de itens.
* GET /api/rh/payroll-runs/{id}/diagnose
*/
@GetMapping("/payroll-runs/{id}/diagnose")
public ResponseEntity<java.util.Map<String, Object>> diagnosePayrollRun(@PathVariable("id") UUID id) {
try {
java.util.Map<String, Object> diagnosis = payrollService.diagnosePayrollRun(id);
return ResponseEntity.ok(diagnosis);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
}
@@ -0,0 +1,46 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.PerformanceEvaluationDTO;
import br.gov.sigefp.rh.service.PerformanceEvaluationService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/**
* Controller REST para gestão de avaliações de desempenho.
*/
@RestController
@RequestMapping("/api/rh/evaluations")
@RequiredArgsConstructor
public class PerformanceEvaluationController {
private final PerformanceEvaluationService evaluationService;
@GetMapping
public ResponseEntity<Page<PerformanceEvaluationDTO>> findAll(
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "sortDirection", required = false, defaultValue = "DESC") String sortDirection) {
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.DESC, "evaluationDate");
Pageable pageable = PageRequest.of(page, size, sort);
Page<PerformanceEvaluationDTO> result = evaluationService.findAll(pageable);
return ResponseEntity.ok(result);
}
@PostMapping("/{id}/finalize")
public ResponseEntity<Void> finalizeEvaluation(@PathVariable UUID id) {
evaluationService.finalizeEvaluation(id);
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,98 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.api.dto.*;
import br.gov.sigefp.rh.service.SalaryStructureService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/api/rh/salary-structure")
@RequiredArgsConstructor
public class SalaryStructureController {
private final SalaryStructureService service;
@GetMapping("/summary")
public ResponseEntity<SalaryStructureFullDTO> getFullStructure() {
return ResponseEntity.ok(service.getFullStructure());
}
// --- Categories ---
@GetMapping("/categories")
public ResponseEntity<List<SalaryCategoryDTO>> getAllCategories() {
return ResponseEntity.ok(service.getAllCategories());
}
@PostMapping("/categories")
public ResponseEntity<SalaryCategoryDTO> createCategory(@Valid @RequestBody SalaryCategoryDTO dto) {
return ResponseEntity.ok(service.createCategory(dto));
}
@PutMapping("/categories/{id}")
public ResponseEntity<SalaryCategoryDTO> updateCategory(@PathVariable("id") UUID id,
@Valid @RequestBody SalaryCategoryDTO dto) {
return ResponseEntity.ok(service.updateCategory(id, dto));
}
@DeleteMapping("/categories/{id}")
public ResponseEntity<Void> deleteCategory(@PathVariable("id") UUID id) {
service.deleteCategory(id);
return ResponseEntity.noContent().build();
}
// --- Grades ---
@GetMapping("/categories/{categoryId}/grades")
public ResponseEntity<List<SalaryGradeDTO>> getGradesByCategory(@PathVariable("categoryId") UUID categoryId) {
return ResponseEntity.ok(service.getGradesByCategory(categoryId));
}
@PostMapping("/grades")
public ResponseEntity<SalaryGradeDTO> createGrade(@Valid @RequestBody SalaryGradeDTO dto) {
return ResponseEntity.ok(service.createGrade(dto));
}
@PutMapping("/grades/{id}")
public ResponseEntity<SalaryGradeDTO> updateGrade(@PathVariable("id") UUID id,
@Valid @RequestBody SalaryGradeDTO dto) {
return ResponseEntity.ok(service.updateGrade(id, dto));
}
@DeleteMapping("/grades/{id}")
public ResponseEntity<Void> deleteGrade(@PathVariable("id") UUID id) {
service.deleteGrade(id);
return ResponseEntity.noContent().build();
}
// --- Steps ---
@GetMapping("/grades/{gradeId}/steps")
public ResponseEntity<List<SalaryStepDTO>> getStepsByGrade(@PathVariable("gradeId") UUID gradeId) {
return ResponseEntity.ok(service.getStepsByGrade(gradeId));
}
@PostMapping("/steps")
public ResponseEntity<SalaryStepDTO> createStep(@Valid @RequestBody SalaryStepDTO dto) {
return ResponseEntity.ok(service.createStep(dto));
}
@DeleteMapping("/steps/{id}")
public ResponseEntity<Void> deleteStep(@PathVariable("id") UUID id) {
service.deleteStep(id);
return ResponseEntity.noContent().build();
}
// --- Grid ---
@GetMapping("/steps/{stepId}/grid")
public ResponseEntity<List<SalaryGridDTO>> getGridByStep(@PathVariable("stepId") UUID stepId) {
return ResponseEntity.ok(service.getGridByStep(stepId));
}
@PostMapping("/grid")
public ResponseEntity<SalaryGridDTO> createGridEntry(@Valid @RequestBody SalaryGridDTO dto) {
return ResponseEntity.ok(service.createGridEntry(dto));
}
}
@@ -0,0 +1,78 @@
package br.gov.sigefp.rh.api;
import br.gov.sigefp.rh.domain.GlobalDeductionRule;
import br.gov.sigefp.rh.domain.TaxBracket;
import br.gov.sigefp.rh.repository.TaxBracketRepository;
import br.gov.sigefp.rh.service.TaxService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
/**
* Controller para parametrização de impostos e regras tributárias.
*/
@RestController
@RequestMapping("/api/rh/taxes")
@RequiredArgsConstructor
public class TaxController {
private final TaxService taxService;
private final TaxBracketRepository taxBracketRepository;
@GetMapping("/rules")
public List<GlobalDeductionRule> findAllRules() {
return taxService.findAllRules();
}
@PostMapping("/rules")
public GlobalDeductionRule saveRule(@RequestBody GlobalDeductionRule rule) {
return taxService.saveRule(rule);
}
@DeleteMapping("/rules/{id}")
public ResponseEntity<Void> deleteRule(@PathVariable UUID id) {
taxService.deleteRule(id);
return ResponseEntity.ok().build();
}
@GetMapping("/brackets")
public List<TaxBracket> findAllBrackets() {
return taxService.findAllBrackets();
}
@PostMapping("/brackets")
public TaxBracket saveBracket(@RequestBody TaxBracket bracket) {
return taxService.saveBracket(bracket);
}
@DeleteMapping("/brackets/{id}")
public ResponseEntity<Void> deleteBracket(@PathVariable UUID id) {
taxService.deleteBracket(id);
return ResponseEntity.ok().build();
}
@GetMapping("/brackets/active")
public List<TaxBracket> getActiveBrackets() {
return taxBracketRepository.findActiveBrackets(LocalDate.now());
}
@GetMapping("/types")
public List<br.gov.sigefp.rh.domain.DeductionType> getDeductionTypes() {
return taxService.findAllDeductionTypes();
}
@PostMapping("/types")
public br.gov.sigefp.rh.domain.DeductionType saveDeductionType(
@RequestBody br.gov.sigefp.rh.domain.DeductionType type) {
return taxService.saveDeductionType(type);
}
@PutMapping("/types/{id}/toggle")
public void toggleDeductionType(@PathVariable UUID id) {
taxService.toggleDeductionTypeStatus(id);
}
}
@@ -0,0 +1,24 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AbsenceDTO {
private UUID id;
private UUID agentId;
private String agentName;
private LocalDate startDate;
private LocalDate endDate;
private Integer days;
private String reason;
private boolean justified;
}
@@ -0,0 +1,24 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* DTO para transferência de dados de conta bancária do agente.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentBankAccountDTO {
private UUID id;
private String bank;
private String branchCode;
private String accountNumber;
private Boolean isPrimary;
}
@@ -0,0 +1,37 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
/**
* DTO para transferência de dados de contrato do agente.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentContractDTO {
private UUID id;
private String contractType;
private LocalDate startDate;
private LocalDate endDate;
private BigDecimal weeklyHours;
private BigDecimal baseSalaryRef;
// Referências para estrutura organizacional e salarial
private UUID orgUnit;
private UUID position;
private UUID salaryCategory;
private UUID salaryGrade;
private UUID salaryStep;
private String legalActReference;
private Boolean isActive;
}
@@ -0,0 +1,97 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
/**
* DTO para transferência de dados de agente.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentDTO {
private UUID id;
@NotBlank(message = "Matrícula é obrigatória")
@Size(min = 1, max = 20, message = "Matrícula deve ter entre 1 e 20 caracteres")
private String matricula;
@NotBlank(message = "NIF é obrigatório")
@Size(min = 9, max = 20, message = "NIF deve ter entre 9 e 20 caracteres")
private String nif;
@NotBlank(message = "Número do BI é obrigatório")
@Size(min = 1, max = 20, message = "Número do BI deve ter entre 1 e 20 caracteres")
private String biNumber;
@NotBlank(message = "Nome completo é obrigatório")
@Size(max = 200, message = "Nome completo deve ter no máximo 200 caracteres")
private String fullName;
@NotNull(message = "Data de nascimento é obrigatória")
private LocalDate birthDate;
@NotNull(message = "Data de admissão é obrigatória")
private LocalDate hireDate;
private LocalDate posseDate;
private LocalDate terminationDate;
private String appointmentType;
private String functionalSituation;
private Integer eligibleDependentsCount;
@Size(max = 20, message = "Status deve ter no máximo 20 caracteres")
private String status;
private String literaryQualification;
// DEPRECATED: Estes campos agora estão em AgentContract
@Deprecated
private UUID salaryCategory;
@Deprecated
private UUID salaryGrade;
@Deprecated
private UUID salaryStep;
@Deprecated
private UUID orgUnit;
@Deprecated
private UUID position;
@Size(max = 50, message = "Nacionalidade deve ter no máximo 50 caracteres")
private String nationality;
@Size(max = 20, message = "Telefone deve ter no máximo 20 caracteres")
private String phone;
@Size(max = 100, message = "Email deve ter no máximo 100 caracteres")
private String email;
@Size(max = 500, message = "Endereço deve ter no máximo 500 caracteres")
private String address;
@Size(max = 500, message = "Motivo da alteração de status deve ter no máximo 500 caracteres")
private String statusChangeReason;
// Campos adicionais para Vida Laboral durante a Edição
private LocalDate eventEffectiveDate;
private LocalDate eventPublicationDate;
private String eventDocumentRef;
// Novos campos para incluir dados relacionados
private AgentContractDTO activeContract;
private AgentBankAccountDTO primaryBankAccount;
}
@@ -0,0 +1,36 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentImportDTO {
// Dados Pessoais
private String nome;
private String nif;
private String matricula; // Número de matrícula único
private String bi;
private String dataNascimento; // dd/MM/yyyy
private String sexo; // M/F
private String estadoCivil; // SOLTEIRO, CASADO
private String habilitacaoLiteraria; // Code
// Dados Funcionais
private String categoriaCodigo;
private String unidadeOrgCodigo;
private String nomeacao; // Data de nomeação dd/MM/yyyy
// Dados do Contrato (Opcional)
private String tipoContrato; // NOMEACAO, etc.
private String dataInicioContrato; // dd/MM/yyyy
private String dataFimContrato; // dd/MM/yyyy
// Dados Bancários (Opcional)
private String bancoCodigo;
private String iban;
}
@@ -0,0 +1,18 @@
package br.gov.sigefp.rh.api.dto;
import lombok.Builder;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
public class AgentImportResultDTO {
private int totalProcessed;
private int successCount;
private int failureCount;
@Builder.Default
private List<String> errors = new ArrayList<>();
}
@@ -0,0 +1,18 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentStatsDTO {
private long total;
private long active;
private long inactive;
private long suspended;
private long terminated;
}
@@ -0,0 +1,27 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AgentStatusHistoryDTO {
private UUID id;
private String previousStatus;
private String newStatus;
private String previousFunctionalSituation;
private String newFunctionalSituation;
private String eventType;
private String reason;
private LocalDateTime changedAt;
private String changedBy;
private String changeLog;
}
@@ -0,0 +1,19 @@
package br.gov.sigefp.rh.api.dto;
import br.gov.sigefp.rh.domain.AttendanceType;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDate;
import java.util.UUID;
@Data
@Builder
public class AttendanceRecordDTO {
private UUID id;
private UUID agentId;
private String agentName;
private LocalDate date;
private AttendanceType type;
private String observation;
}
@@ -0,0 +1,19 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CareerRegimeDTO {
private UUID id;
private String code;
private String name;
private String description;
}
@@ -0,0 +1,25 @@
package br.gov.sigefp.rh.api.dto;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Builder
public class CareerTimelineDTO {
private LocalDate date;
private String eventType;
private String eventTypeName; // Nome amigável em português
private String reason;
private String documentRef;
// Impactos Financeiros
private BigDecimal totalBaseAmount;
private BigDecimal cargoAmount; // 5/6
private BigDecimal exercicioAmount; // 1/6
// Mudanças representadas como texto para facilitar o frontend
private String changeSummary;
}
@@ -0,0 +1,25 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateAbsenceDTO {
@NotNull
private UUID agentId;
@NotNull
private LocalDate startDate;
@NotNull
private LocalDate endDate;
private String reason;
private boolean justified;
}
@@ -0,0 +1,38 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
/**
* DTO para criação de período de folha.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreatePayrollPeriodDTO {
@NotNull(message = "Ano fiscal é obrigatório")
@Min(value = 2000, message = "Ano fiscal deve ser maior ou igual a 2000")
@Max(value = 2100, message = "Ano fiscal deve ser menor ou igual a 2100")
private Integer fiscalYear;
@NotNull(message = "Mês é obrigatório")
@Min(value = 1, message = "Mês deve estar entre 1 e 12")
@Max(value = 12, message = "Mês deve estar entre 1 e 12")
private Integer month;
@NotNull(message = "Data de início é obrigatória")
private LocalDate startDate;
@NotNull(message = "Data de fim é obrigatória")
private LocalDate endDate;
}
@@ -0,0 +1,30 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* DTO para criação de execução de folha.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreatePayrollRunDTO {
@NotNull(message = "ID do período é obrigatório")
private UUID periodId;
private UUID ministryId; // Opcional: se não fornecido, processa todos os ministérios
private UUID orgUnitId; // Opcional: se não fornecido, processa todas as unidades
@NotNull(message = "Tipo de execução é obrigatório")
private String runType; // REGULAR, BONUS, ADJUSTMENT, etc.
}
@@ -0,0 +1,18 @@
package br.gov.sigefp.rh.api.dto;
import br.gov.sigefp.rh.domain.AttendanceSheetStatus;
import lombok.Builder;
import lombok.Data;
import java.util.UUID;
@Data
@Builder
public class MonthlyAttendanceSheetDTO {
private UUID id;
private UUID orgUnitId;
private Integer month;
private Integer year;
private AttendanceSheetStatus status;
private String approvedBy;
}
@@ -0,0 +1,41 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.UUID;
/**
* DTO para transferência de dados de item de folha.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayrollItemDTO {
private UUID id;
private UUID agent;
private String agentName; // Nome do agente para exibição
private String lineType;
private UUID earningTypeId;
private UUID deductionTypeId;
private String description;
private BigDecimal quantity;
private BigDecimal unitAmount;
private BigDecimal totalAmount;
private UUID budgetLine;
}
@@ -0,0 +1,32 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
/**
* DTO para transferência de dados de período de folha.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayrollPeriodDTO {
private UUID id;
private Integer fiscalYear;
private Integer month;
private String status;
private LocalDate startDate;
private LocalDate endDate;
}
@@ -0,0 +1,42 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* DTO para transferência de dados de execução de folha.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayrollRunDTO {
private UUID id;
private UUID periodId;
private UUID ministry;
private String ministryName;
private UUID orgUnit;
private String orgUnitName;
private String runType;
private String status;
private LocalDateTime createdAt;
private UUID createdBy;
private List<PayrollItemDTO> items;
}
@@ -0,0 +1,31 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.UUID;
/**
* DTO para transferência de dados de avaliação de desempenho.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PerformanceEvaluationDTO {
private UUID id;
private UUID agentId;
private String agentName;
private String agentMatricula;
private Integer referenceYear;
private Integer score;
private String status; // DRAFT, FINAL, CANCELLED
private String mention; // MAU, MEDIOCRE, REGULAR, BOM, MUITO_BOM
private String observations;
private LocalDate evaluationDate;
}
@@ -0,0 +1,26 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalaryCategoryDTO {
private UUID id;
@NotBlank(message = "Código é obrigatório")
private String code;
@NotBlank(message = "Nome é obrigatório")
private String name;
private UUID regimeId;
private String regimeName;
}
@@ -0,0 +1,27 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalaryGradeDTO {
private UUID id;
@NotNull(message = "Categoria é obrigatória")
private UUID categoryId;
@NotBlank(message = "Código é obrigatório")
private String code;
@NotBlank(message = "Nome é obrigatório")
private String name;
}
@@ -0,0 +1,33 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalaryGridDTO {
private UUID id;
@NotNull(message = "Step é obrigatório")
private UUID stepId;
@NotNull(message = "Data de início da vigência é obrigatória")
private LocalDate validFrom;
private LocalDate validTo;
@NotNull(message = "Valor base é obrigatório")
private BigDecimal baseAmount;
private BigDecimal subsidyAmount;
private BigDecimal grossAmount;
}
@@ -0,0 +1,23 @@
package br.gov.sigefp.rh.api.dto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalaryStepDTO {
private UUID id;
@NotNull(message = "Grau (Grade) é obrigatório")
private UUID gradeId;
@NotNull(message = "Número do step é obrigatório")
private Integer stepNumber;
}
@@ -0,0 +1,52 @@
package br.gov.sigefp.rh.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalaryStructureFullDTO {
private List<CategoryGroupDTO> categories;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CategoryGroupDTO {
private UUID id;
private String code;
private String name;
private String regimeName;
private List<GradeRowDTO> grades;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class GradeRowDTO {
private UUID id;
private String code;
private String name;
private List<StepDetailDTO> steps;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class StepDetailDTO {
private UUID id;
private Integer stepNumber;
private Double currentValue;
private Double subsidyAmount;
private Double grossAmount;
}
}
@@ -0,0 +1,196 @@
package br.gov.sigefp.rh.config;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import org.springframework.core.annotation.Order;
@Slf4j
@Component
@RequiredArgsConstructor
@Order(2)
@Transactional
public class PayrollDataInitializer implements CommandLineRunner {
private final EarningTypeRepository earningTypeRepository;
private final DeductionTypeRepository deductionTypeRepository;
private final GlobalDeductionRuleRepository globalDeductionRuleRepository;
private final TaxBracketRepository taxBracketRepository;
private final PayrollPeriodRepository payrollPeriodRepository;
private final SalaryCategoryRepository salaryCategoryRepository;
private final SalaryGradeRepository salaryGradeRepository;
private final SalaryStepRepository salaryStepRepository;
private final SalaryGridRepository salaryGridRepository;
private final AgentRepository agentRepository;
private final br.gov.sigefp.budget.repository.BudgetLineRepository budgetLineRepository;
private final br.gov.sigefp.budget.repository.FiscalYearRepository fiscalYearRepository;
private final FamilyAllowanceTableRepository familyAllowanceTableRepository;
@Override
// @Transactional removed to allow partial commits
public void run(String... args) {
log.info("Inicializando tipos de proventos, descontos e regras tributárias...");
// 1. Proventos
createEarningTypeIfNotExist("SALARIO_BASE", "Vencimento Base", true, "311100");
createEarningTypeIfNotExist("SUBSIDIO", "Subsídio de Representação", true, "311102");
createEarningTypeIfNotExist("ABONO_FAMILIA", "Abono de Família", false, "312101");
// 1.1 Tabela de Abono de Família
for (int i = 1; i <= 10; i++) {
createFamilyAllowanceIfNotExist(i, new BigDecimal("1000").multiply(new BigDecimal(i)));
}
// 2. Descontos
DeductionType inps = createDeductionTypeIfNotExist("INPS", "Segurança Social (INPS)", "312100");
DeductionType irps = createDeductionTypeIfNotExist("IRPS", "Imposto Profissional (IRPS)", null);
DeductionType demo = createDeductionTypeIfNotExist("ID", "Imposto de Democracia", null);
DeductionType selo = createDeductionTypeIfNotExist("SELO", "Imposto de Selo", null);
DeductionType falta = createDeductionTypeIfNotExist("FALTA_INJUSTIFICADA", "Falta Injustificada", null);
// 3. Regras Globais (Válidas desde 2024 até o infinito por padrão)
createGlobalRuleIfNotExist(inps, new BigDecimal("0.07"));
createGlobalRuleIfNotExist(selo, new BigDecimal("0.003"));
// Limpar tabela de escalões para garantir conformidade com a nova lei
// Limpar tabela de escalões para garantir conformidade com a nova lei
// try {
// taxBracketRepository.deleteAllInBatch();
// log.info("Tabela de escalões limpa com sucesso.");
// } catch (Exception e) {
// log.error("Erro ao limpar tabela de escalões: {}", e.getMessage());
// }
createTaxBracketIfNotExist(irps, "0.00", "41667.00", "0.0100", "0.00", null);
createTaxBracketIfNotExist(irps, "41668.00", "83333.00", "0.0600", "2083.00", null);
createTaxBracketIfNotExist(irps, "83334.00", "208333.00", "0.0800", "3750.00", null);
createTaxBracketIfNotExist(irps, "208334.00", "300000.00", "0.1000", "7917.00", null);
createTaxBracketIfNotExist(irps, "300001.00", "400500.00", "0.1200", "13917.00", null);
createTaxBracketIfNotExist(irps, "400501.00", "750000.00", "0.1400", "21927.00", null);
createTaxBracketIfNotExist(irps, "750001.00", "1100000.00", "0.1600", "36927.00", null);
createTaxBracketIfNotExist(irps, "1100001.00", "1500000.00", "0.1800", "58927.00", null);
createTaxBracketIfNotExist(irps, "1500001.00", null, "0.2000", "88929.00", null);
// 6. Imposto de Democracia (Novas Regras)
// createTaxBracketIfNotExist(demo, "0.00", "41667.00", null, null, "500.00");
// createTaxBracketIfNotExist(demo, "41668.00", "83333.00", null, null,
// "1000.00");
// createTaxBracketIfNotExist(demo, "83334.00", "208333.00", null, null,
// "2000.00");
// createTaxBracketIfNotExist(demo, "208334.00", "300000.00", null, null,
// "4000.00");
// createTaxBracketIfNotExist(demo, "300001.00", "405500.00", null, null,
// "6000.00");
// createTaxBracketIfNotExist(demo, "405501.00", "750000.00", null, null,
// "10000.00");
// createTaxBracketIfNotExist(demo, "750001.00", "1100000.00", null, null,
// "15000.00");
// createTaxBracketIfNotExist(demo, "1100001.00", "1500000.00", null, null,
// "17000.00");
// createTaxBracketIfNotExist(demo, "1500001.00", null, null, null, "20000.00");
// 5. Estrutura Salarial e Grelha
// 5. Estrutura Salarial e Grelha
try {
// initializeSalaryAndPeriod(); // Disable to prevent hang
log.info("Inicialização de Salários pulada para garantir boot.");
} catch (Exception e) {
log.error("Erro ao inicializar estrutura salarial: {}", e.getMessage());
}
log.info("Configuração tributária e operacional concluída.");
}
// Helper atualizado para suportar Taxa Progressiva OU Valor Fixo
private void createTaxBracketIfNotExist(DeductionType type, String lower, String upper, String rate,
String excess, String fixed) {
BigDecimal low = new BigDecimal(lower);
BigDecimal up = upper != null ? new BigDecimal(upper) : null;
// Verifica duplicidade
List<TaxBracket> active = taxBracketRepository.findActiveBrackets(LocalDate.now());
boolean exists = active.stream().anyMatch(
b -> b.getDeductionType().getCode().equals(type.getCode()) && b.getLowerLimit().compareTo(low) == 0);
if (!exists) {
TaxBracket bracket = TaxBracket.builder()
.deductionType(type)
.lowerLimit(low)
.upperLimit(up)
.ratePercentage(rate != null ? new BigDecimal(rate) : null)
.excessDeduction(excess != null ? new BigDecimal(excess) : null)
.fixedAmount(fixed != null ? new BigDecimal(fixed) : null)
.validFrom(LocalDate.of(2024, 1, 1))
.build();
taxBracketRepository.save(bracket);
log.info("Escalão {} criado: {} a {}", type.getCode(), lower, upper != null ? upper : "inf");
}
}
private void createEarningTypeIfNotExist(String code, String name, boolean taxable, String econClass) {
if (earningTypeRepository.findByCode(code).isEmpty()) {
EarningType type = EarningType.builder()
.code(code)
.name(name)
.taxable(taxable)
.economicClassCode(econClass)
.build();
earningTypeRepository.save(type);
log.info("Tipo de Provento criado: {}", code);
}
}
private DeductionType createDeductionTypeIfNotExist(String code, String name, String econClass) {
return deductionTypeRepository.findByCode(code).orElseGet(() -> {
DeductionType type = DeductionType.builder()
.code(code)
.name(name)
.economicClassCode(econClass)
.build();
log.info("Tipo de Desconto criado: {}", code);
return deductionTypeRepository.save(type);
});
}
private void createGlobalRuleIfNotExist(DeductionType type, BigDecimal percentage) {
// Optimized: Check existence via count/exists query to avoid
// LazyInitializationException
boolean exists = globalDeductionRuleRepository.findAll().stream()
.anyMatch(r -> r.getDeductionType() != null && r.getDeductionType().getId().equals(type.getId()));
if (!exists) {
GlobalDeductionRule rule = GlobalDeductionRule.builder()
.deductionType(type)
.percentage(percentage)
.amountFixed(null)
.validFrom(LocalDate.of(2024, 1, 1))
.build();
globalDeductionRuleRepository.save(rule);
log.info("Regra Global criada: {} ({}%)", type.getName(), percentage.multiply(new BigDecimal(100)));
log.info("Regra Global criada: {} ({}%)", type.getName(), percentage.multiply(new BigDecimal(100)));
}
}
private void createFamilyAllowanceIfNotExist(Integer dependents, BigDecimal amount) {
// Simple check to avoid LazyInitializationException usually not needed for
// simple find
if (familyAllowanceTableRepository.findByDependentsAndDate(dependents, LocalDate.now()).isEmpty()) {
FamilyAllowanceTable table = FamilyAllowanceTable.builder()
.dependentsCount(dependents)
.amount(amount)
.validFrom(LocalDate.of(2024, 1, 1))
.build();
familyAllowanceTableRepository.save(table);
log.info("Abono de Família configurado: {} dependentes = {}", dependents, amount);
}
}
}
@@ -0,0 +1,43 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
@Entity
@Table(name = "agent_absence")
@Getter
@Setter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class Absence extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(name = "start_date", nullable = false)
private LocalDate startDate;
@Column(name = "end_date", nullable = false)
private LocalDate endDate;
@Column(name = "days", nullable = false)
private Integer days;
@Column(name = "reason")
private String reason;
@Column(name = "is_justified", nullable = false)
private boolean justified;
@Column(name = "deducted_in_payroll_run_id")
private java.util.UUID deductedInPayrollRunId;
}
@@ -0,0 +1,107 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Entidade que representa um agente/funcionário público.
*/
@Entity
@Table(name = "agents", indexes = {
@Index(name = "idx_agent_nif", columnList = "nif"),
@Index(name = "idx_agent_bi", columnList = "bi_number"),
@Index(name = "idx_agent_matricula", columnList = "matricula"),
@Index(name = "idx_agent_status", columnList = "status")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class Agent extends AuditableEntity {
@Column(nullable = false, unique = true, length = 20)
private String matricula; // Número de matrícula único
@Column(nullable = false, unique = true, length = 20)
private String nif; // Número de Identificação Fiscal
@Column(nullable = false, unique = true, length = 20, name = "bi_number")
private String biNumber; // Número do Bilhete de Identidade
@Column(nullable = false, length = 200)
private String fullName;
@Column(nullable = false)
private LocalDate birthDate;
@Column(nullable = false)
private LocalDate hireDate; // Data de admissão
@Column(name = "posse_date")
private LocalDate posseDate; // Data de posse
private LocalDate terminationDate; // Data de desligamento (null se ativo)
@Column(nullable = false, length = 50, name = "appointment_type")
private String appointmentType = "PROVISORIA"; // PROVISORIA, DEFINITIVA, CONTRATO_PROVIMENTO, CONTRATO_TERMO
@Column(nullable = false, length = 50, name = "functional_situation")
private String functionalSituation = "ATIVIDADE_NO_QUADRO"; // ATIVIDADE_NO_QUADRO, ATIVIDADE_FORA_DO_QUADRO...
@Column(name = "eligible_dependents_count")
private Integer eligibleDependentsCount; // Número de dependentes para Abono de Família
@Column(nullable = false, length = 20)
private String status = "REGISTERED"; // REGISTERED, ACTIVE, INACTIVE, SUSPENDED, TERMINATED
@Column(name = "literary_qualification", length = 100)
private String literaryQualification; // Habilitação Literária
@Column(name = "salary_category_id")
private UUID salaryCategory;
@Column(name = "salary_grade_id")
private UUID salaryGrade;
@Column(name = "salary_step_id")
private UUID salaryStep;
@Column(name = "org_unit_id")
private UUID orgUnit; // Referência à unidade organizacional
@Column(name = "position_id")
private UUID position; // Referência à posição/cargo
@Column(length = 50)
private String nationality;
@Column(length = 20)
private String phone;
@Column(length = 100)
private String email;
@Column(length = 500)
private String address;
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true)
@lombok.Builder.Default
private List<AgentContract> contracts = new ArrayList<>();
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true)
@lombok.Builder.Default
private List<AgentBankAccount> bankAccounts = new ArrayList<>();
@OneToMany(mappedBy = "agent", cascade = CascadeType.ALL, orphanRemoval = true)
@lombok.Builder.Default
private List<AgentDeductionRule> deductionRules = new ArrayList<>();
}
@@ -0,0 +1,51 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import java.util.UUID;
/**
* Entidade que representa uma conta bancária de um agente.
*/
@Entity
@Table(name = "agent_bank_account", indexes = {
@Index(name = "idx_bank_account_agent", columnList = "agent_id"),
@Index(name = "idx_bank_account_primary", columnList = "agent_id,is_primary")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@lombok.experimental.SuperBuilder
public class AgentBankAccount extends AuditableEntity {
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@JsonProperty("agentId")
public UUID getAgentIdForSerialization() {
return agent != null ? agent.getId() : null;
}
@Column(nullable = false, length = 100)
private String bank; // Nome do banco
@Column(nullable = false, length = 20, name = "branch_code")
private String branchCode; // Código da agência
@Column(nullable = false, length = 50, name = "account_number")
private String accountNumber; // Número da conta
@Column(length = 50)
private String iban; // IBAN da conta
@Column(nullable = false, name = "is_primary")
@Builder.Default
private Boolean isPrimary = false; // Conta principal para pagamentos
}
@@ -0,0 +1,75 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
/**
* Entidade que representa um contrato de trabalho do agente.
* Usa LocalDate para datas, evitando strings concatenadas.
*/
@Entity
@Table(name = "agent_contract", indexes = {
@Index(name = "idx_contract_agent", columnList = "agent_id"),
@Index(name = "idx_contract_dates", columnList = "start_date,end_date"),
@Index(name = "idx_contract_active", columnList = "is_active")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AgentContract extends AuditableEntity {
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(nullable = false, length = 50)
private String contractType; // PERMANENT, TEMPORARY, INTERNSHIP, FIXED_TERM, etc.
@Column(nullable = false, name = "start_date")
private LocalDate startDate;
@Column(name = "end_date")
private LocalDate endDate; // Null para contratos sem término definido
@Column(name = "weekly_hours", precision = 5, scale = 2)
private BigDecimal weeklyHours; // Horas semanais
@Column(name = "base_salary_ref", precision = 19, scale = 2)
private BigDecimal baseSalaryRef; // Referência salarial base
@Column(name = "org_unit_id")
private UUID orgUnit;
@Column(name = "position_id")
private UUID position;
@Column(name = "salary_category_id")
private UUID salaryCategory;
@Column(name = "salary_grade_id")
private UUID salaryGrade;
@Column(name = "salary_step_id")
private UUID salaryStep;
@Column(name = "legal_act_reference", length = 100)
private String legalActReference; // Ex: Decreto n┬║ 12/2023, Despacho n┬║ 45/2023
@Column(nullable = false, name = "is_active")
@Builder.Default
private Boolean isActive = true;
@JsonProperty("agentId")
public UUID getAgentIdForSerialization() {
return agent != null ? agent.getId() : null;
}
}
@@ -0,0 +1,55 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
/**
* Entidade que representa uma regra de desconto específica para um agente.
* Usa LocalDate para validade, evitando strings concatenadas.
*/
@Entity
@Table(name = "agent_deduction_rule", indexes = {
@Index(name = "idx_deduction_rule_agent", columnList = "agent_id"),
@Index(name = "idx_deduction_rule_type", columnList = "deduction_type_id"),
@Index(name = "idx_deduction_rule_validity", columnList = "valid_from,valid_to")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AgentDeductionRule extends AuditableEntity {
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@JsonProperty("agentId")
public UUID getAgentIdForSerialization() {
return agent != null ? agent.getId() : null;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deduction_type_id", nullable = false)
private DeductionType deductionType;
@Column(name = "amount_fixed", precision = 19, scale = 2)
private BigDecimal amountFixed; // Valor fixo (se aplicável)
@Column(precision = 5, scale = 2)
private BigDecimal percentage; // Percentual (se aplicável)
@Column(nullable = false, name = "valid_from")
private LocalDate validFrom; // Data de início da vigência
@Column(name = "valid_to")
private LocalDate validTo; // Data de fim da vigência (null se ainda vigente)
}
@@ -0,0 +1,57 @@
package br.gov.sigefp.rh.domain;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.UuidGenerator;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "agent_status_history", indexes = {
@Index(name = "idx_history_agent_id", columnList = "agent_id"),
@Index(name = "idx_history_changed_at", columnList = "changed_at")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AgentStatusHistory {
@Id
@GeneratedValue
@UuidGenerator
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(name = "previous_status", length = 20)
private String previousStatus;
@Column(name = "new_status", length = 20)
private String newStatus;
@Column(name = "previous_functional_situation", length = 50)
private String previousFunctionalSituation;
@Column(name = "new_functional_situation", length = 50)
private String newFunctionalSituation;
@Column(name = "event_type", length = 50)
private String eventType; // PROMOTIO, PROGRESSION, SUBSTITUTION, TRANSFER, etc.
@Column(length = 500)
private String reason;
@Column(name = "changed_at", nullable = false)
private LocalDateTime changedAt;
@Column(name = "changed_by", length = 100)
private String changedBy;
@Column(name = "change_log", length = 1000)
private String changeLog;
}
@@ -0,0 +1,39 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
@Entity
@Table(name = "attendance_record")
@Getter
@Setter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class AttendanceRecord extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sheet_id", nullable = false)
private MonthlyAttendanceSheet sheet;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(name = "date", nullable = false)
private LocalDate date;
@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false)
private AttendanceType type;
@Column(name = "observation")
private String observation;
}
@@ -0,0 +1,8 @@
package br.gov.sigefp.rh.domain;
public enum AttendanceSheetStatus {
DRAFT, // Em preenchimento
SUBMITTED, // Enviado para aprovação
APPROVED, // Aprovado
CLOSED // Processado (Bloqueado)
}
@@ -0,0 +1,8 @@
package br.gov.sigefp.rh.domain;
public enum AttendanceType {
ABSENCE_UNJUSTIFIED, // Falta Injustificada
ABSENCE_JUSTIFIED, // Falta Justificada
SICK_LEAVE, // Baixa Médica
VACATION // Férias
}
@@ -0,0 +1,93 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
/**
* Entidade que registra um evento estruturado na vida laboral/carreira do
* agente.
* Substitui o AgentStatusHistory para fornecer dados ricos para folha e
* auditoria.
*/
@Entity
@Table(name = "career_events", indexes = {
@Index(name = "idx_career_event_agent", columnList = "agent_id"),
@Index(name = "idx_career_event_type", columnList = "event_type"),
@Index(name = "idx_career_event_date", columnList = "effective_date")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CareerEvent extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Enumerated(EnumType.STRING)
@Column(name = "event_type", nullable = false, length = 50)
private CareerEventType eventType;
@Column(name = "effective_date", nullable = false)
private LocalDate effectiveDate; // Data em que o evento passa a valer
@Column(name = "publication_date")
private LocalDate publicationDate; // Data de publicação no Boletim Oficial
@Column(name = "document_ref", length = 100)
private String documentRef; // Referência do Despacho ou documento legal
// Snapshots da Carreira Salarial
private UUID previousCategory;
private UUID newCategory;
private UUID previousGrade;
private UUID newGrade;
private UUID previousStep;
private UUID newStep;
// Snapshots Organizacionais
private UUID previousOrgUnit;
private UUID newOrgUnit;
private UUID previousPosition;
private UUID newPosition;
// Snapshot Financeiro (Baseado no Decreto 12-A/94)
@Column(precision = 19, scale = 2)
private BigDecimal totalBaseAmount; // Rb (Remuneração Base)
@Column(precision = 19, scale = 2)
private BigDecimal cargoAmount; // 5/6 da remuneração base
@Column(precision = 19, scale = 2)
private BigDecimal exercicioAmount; // 1/6 da remuneração base
@Column(length = 500)
private String reason;
@Column(name = "created_by_user")
private String createdByUser;
/**
* Calcula a divisão 5/6 e 1/6 com base no valor base total.
*/
public void calculateFinancialSplit(BigDecimal total) {
if (total == null)
return;
this.totalBaseAmount = total;
// 5/6 = (total * 5) / 6
this.cargoAmount = total.multiply(BigDecimal.valueOf(5))
.divide(BigDecimal.valueOf(6), 2, java.math.RoundingMode.HALF_UP);
// 1/6 = total - 5/6 (para garantir que a soma feche perfeitamente)
this.exercicioAmount = total.subtract(this.cargoAmount);
}
}
@@ -0,0 +1,22 @@
package br.gov.sigefp.rh.domain;
/**
* Tipos de eventos que podem ocorrer na vida laboral de um agente público.
* Em conformidade com o Decreto nº 12-A/94 da Guiné-Bissau.
*/
public enum CareerEventType {
ADMISSAO, // Início do vínculo (ingresso)
NOMEACAO_PROVISORIA, // Período probatório (2 anos)
NOMEACAO_DEFINITIVA, // Estabilização após período probatório
PROMOCAO, // Mudança de categoria (ex: de Técnico para Técnico Superior)
PROGRESSAO, // Mudança de escalão dentro da mesma categoria
SUBSTITUICAO, // Exercício temporário de cargo superior (>30 dias)
TRANSFERENCIA, // Mudança de unidade orgânica
RECLASSIFICACAO, // Mudança de carreira por nova habilitação
COMISSAO_SERVICO, // Exercício de cargo de direção ou chefia
DESTACAMENTO, // Trabalho temporário em outra instituição
REQUISICAO, // Cedência temporária a pedido
SUSPENSAO, // Interrupção temporária do exercício
TERMINATION, // Fim do vínculo (exoneração, aposentação, falecimento)
RETIFICACAO // Correção ou alteração de dados de um ato administrativo anterior
}
@@ -0,0 +1,38 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
/**
* Entidade que representa um Regime de Carreira (ex: Regime Geral, Saúde,
* Educação).
* Agrupa categorias salariais por setor.
*/
@Entity
@Table(name = "career_regime", indexes = {
@Index(name = "idx_career_regime_code", columnList = "code")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CareerRegime extends AuditableEntity {
@Column(nullable = false, unique = true, length = 50)
private String code; // Ex: REG-GERAL, REG-SAUDE
@Column(nullable = false, length = 200)
private String name; // Ex: Regime Geral da Função Pública
@Column(length = 500)
private String description;
@OneToMany(mappedBy = "regime", cascade = CascadeType.ALL)
@Builder.Default
private List<SalaryCategory> categories = new ArrayList<>();
}
@@ -0,0 +1,40 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
/**
* Entidade que representa um tipo de desconto/dedução.
*/
@Entity
@Table(name = "deduction_type", indexes = {
@Index(name = "idx_deduction_type_code", columnList = "code")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class DeductionType extends AuditableEntity {
@Column(nullable = false, unique = true, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false)
@Builder.Default
private Boolean mandatory = false; // Se o desconto é obrigatório
@Column(length = 20, name = "economic_class_code")
private String economicClassCode; // Classificador Económico
@Column(nullable = false)
@Builder.Default
private Boolean active = true;
}
@@ -0,0 +1,34 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
/**
* Entidade que representa um tipo de provento/ganho.
*/
@Entity
@Table(name = "earning_type", indexes = {
@Index(name = "idx_earning_type_code", columnList = "code")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class EarningType extends AuditableEntity {
@Column(nullable = false, unique = true, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false)
@Builder.Default
private Boolean taxable = true; // Se o provento é tributável
@Column(length = 20, name = "economic_class_code")
private String economicClassCode; // Classificador Económico (ex: 311100)
}
@@ -0,0 +1,33 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Table(name = "family_allowance_table")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class FamilyAllowanceTable extends BaseEntity {
@Column(nullable = false)
private Integer dependentsCount;
@Column(nullable = false, precision = 19, scale = 2)
private BigDecimal amount;
@Column(nullable = false)
private LocalDate validFrom;
@Column
private LocalDate validTo;
}
@@ -0,0 +1,44 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* Representa uma regra de desconto global (ex: INPS 7%, Imposto de Selo 0.3%).
*/
@Entity
@Table(name = "global_deduction_rule")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class GlobalDeductionRule extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deduction_type_id", nullable = false)
private DeductionType deductionType;
@Column(nullable = false, precision = 5, scale = 4)
private BigDecimal percentage; // Ex: 0.07 para 7%, 0.003 para 0.3%
@Column(precision = 19, scale = 2)
private BigDecimal amountFixed;
@Column(nullable = false, name = "valid_from")
private LocalDate validFrom;
@Column(name = "valid_to")
private LocalDate validTo;
@Builder.Default
@Column(nullable = false)
private boolean active = true;
}
@@ -0,0 +1,36 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
@Entity
@Table(name = "monthly_attendance_sheet", uniqueConstraints = {
@UniqueConstraint(columnNames = { "org_unit_id", "month", "year" }) })
@Getter
@Setter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class MonthlyAttendanceSheet extends BaseEntity {
@Column(name = "org_unit_id", nullable = false)
private java.util.UUID orgUnitId;
@Column(name = "month", nullable = false)
private Integer month;
@Column(name = "year", nullable = false)
private Integer year;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private AttendanceSheetStatus status;
@Column(name = "approved_by")
private String approvedBy;
}
@@ -0,0 +1,59 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.util.UUID;
/**
* Entidade que representa um item de folha de pagamento (provento ou desconto).
*/
@Entity
@Table(name = "payroll_item", indexes = {
@Index(name = "idx_payroll_item_run", columnList = "payroll_run_id"),
@Index(name = "idx_payroll_item_agent", columnList = "agent_id"),
@Index(name = "idx_payroll_item_type", columnList = "line_type")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class PayrollItem extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "payroll_run_id", nullable = false)
private PayrollRun payrollRun;
@Column(name = "agent_id", nullable = false)
private UUID agent; // Referência ao agente
@Column(nullable = false, length = 20, name = "line_type")
private String lineType; // EARNING, DEDUCTION
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "earning_type_id")
private EarningType earningType; // Tipo de provento (se lineType = EARNING)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deduction_type_id")
private DeductionType deductionType; // Tipo de desconto (se lineType = DEDUCTION)
@Column(length = 500)
private String description;
@Column(precision = 10, scale = 2)
private BigDecimal quantity; // Quantidade (horas, dias, etc.)
@Column(precision = 19, scale = 2, name = "unit_amount")
private BigDecimal unitAmount; // Valor unitário
@Column(nullable = false, precision = 19, scale = 2, name = "total_amount")
private BigDecimal totalAmount; // Valor total (quantity * unitAmount ou valor fixo)
@Column(name = "budget_line_id")
private UUID budgetLine; // Referência à linha orçamentária (para rastreabilidade)
}
@@ -0,0 +1,46 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* Entidade que representa um período de folha de pagamento.
* Usa LocalDate para datas, evitando strings concatenadas.
*/
@Entity
@Table(name = "payroll_period", indexes = {
@Index(name = "idx_payroll_period_fiscal", columnList = "fiscal_year,month"),
@Index(name = "idx_payroll_period_status", columnList = "status")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@lombok.experimental.SuperBuilder
public class PayrollPeriod extends AuditableEntity {
@Column(nullable = false, name = "fiscal_year")
private Integer fiscalYear; // Ano fiscal
@Column(nullable = false)
private Integer month; // Mês (1-12)
@Column(nullable = false, length = 20)
@Builder.Default
private String status = "OPEN"; // OPEN, CLOSED, PROCESSING
@Column(nullable = false, name = "start_date")
private LocalDate startDate; // Data de início do período
@Column(nullable = false, name = "end_date")
private LocalDate endDate; // Data de fim do período
@OneToMany(mappedBy = "period", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<PayrollRun> payrollRuns = new ArrayList<>();
}
@@ -0,0 +1,51 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Entidade que representa uma execução/processamento de folha de pagamento.
* Usa LocalDateTime para timestamps.
*/
@Entity
@Table(name = "payroll_run", indexes = {
@Index(name = "idx_payroll_run_period", columnList = "period_id"),
@Index(name = "idx_payroll_run_ministry", columnList = "ministry_id"),
@Index(name = "idx_payroll_run_org_unit", columnList = "org_unit_id"),
@Index(name = "idx_payroll_run_status", columnList = "status")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class PayrollRun extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "period_id", nullable = false)
private PayrollPeriod period;
@Column(name = "ministry_id")
private UUID ministry; // Referência ao ministério
@Column(name = "org_unit_id")
private UUID orgUnit; // Referência à unidade organizacional
@Column(nullable = false, length = 50, name = "run_type")
private String runType; // REGULAR, BONUS, ADJUSTMENT, etc.
@Column(nullable = false, length = 20)
@Builder.Default
private String status = "PENDING"; // PENDING, PROCESSING, COMPLETED, FAILED
@OneToMany(mappedBy = "payrollRun", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<PayrollItem> items = new ArrayList<>();
}
@@ -0,0 +1,67 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.time.LocalDate;
/**
* Representa a avaliação de desempenho anual de um agente.
* Essencial para validar progressões e promoções conforme o Decreto nº 12-A/94.
*/
@Entity
@Table(name = "performance_evaluations", indexes = {
@Index(name = "idx_evaluation_agent", columnList = "agent_id"),
@Index(name = "idx_evaluation_year", columnList = "reference_year"),
@Index(name = "idx_evaluation_status", columnList = "status")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class PerformanceEvaluation extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "agent_id", nullable = false)
private Agent agent;
@Column(nullable = false, name = "reference_year")
private Integer referenceYear; // Ano a que se refere a avaliação
@Column(nullable = false)
private Integer score; // Pontuação 1-10 ou 5-20 (Decreto 12-A/94 sugere quantificação)
@Column(length = 20, nullable = true)
@Builder.Default
private String status = "DRAFT"; // DRAFT, FINAL, CANCELLED
@Column(nullable = false, length = 50)
private String mention; // MAU, MEDIOCRE, REGULAR, BOM, MUITO_BOM
@Column(columnDefinition = "TEXT")
private String observations;
@Column(name = "evaluation_date")
private LocalDate evaluationDate;
/**
* Define a menção qualitativa com base na pontuação quantitativa conforme o
* Estatuto (Escala 0-20).
*/
public void updateMentionFromScore() {
if (score == null)
return;
if (score >= 18)
this.mention = "MUITO_BOM";
else if (score >= 14)
this.mention = "BOM";
else if (score >= 10)
this.mention = "REGULAR";
else if (score >= 6)
this.mention = "MEDIOCRE";
else
this.mention = "MAU";
}
}
@@ -0,0 +1,37 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
/**
* Entidade que representa uma categoria salarial.
*/
@Entity
@Table(name = "salary_category", indexes = {
@Index(name = "idx_salary_category_code", columnList = "code")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SalaryCategory extends AuditableEntity {
@Column(nullable = false, unique = true, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "regime_id") // Nullable initially for migration, but generally should be required
private CareerRegime regime;
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<SalaryGrade> grades = new ArrayList<>();
}
@@ -0,0 +1,39 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
/**
* Entidade que representa um grau salarial dentro de uma categoria.
*/
@Entity
@Table(name = "salary_grade", indexes = {
@Index(name = "idx_salary_grade_code", columnList = "code"),
@Index(name = "idx_salary_grade_category", columnList = "category_id")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SalaryGrade extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
private SalaryCategory category;
@Column(nullable = false, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String name;
@OneToMany(mappedBy = "grade", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<SalaryStep> steps = new ArrayList<>();
}
@@ -0,0 +1,45 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* Entidade que representa a tabela salarial (valores por step em um período).
* Usa LocalDate para validade, evitando strings concatenadas.
*/
@Entity
@Table(name = "salary_grid", indexes = {
@Index(name = "idx_salary_grid_step", columnList = "step_id"),
@Index(name = "idx_salary_grid_validity", columnList = "valid_from,valid_to")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public class SalaryGrid extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "step_id", nullable = false)
private SalaryStep step;
@Column(nullable = false, name = "valid_from")
private LocalDate validFrom; // Data de início da vigência
@Column(name = "valid_to")
private LocalDate validTo; // Data de fim da vigência (null se ainda vigente)
@Column(nullable = false, precision = 19, scale = 2, name = "base_amount")
private BigDecimal baseAmount; // Valor base salarial
@Column(precision = 19, scale = 2, name = "subsidy_amount")
private BigDecimal subsidyAmount; // Subsídio
@Column(precision = 19, scale = 2, name = "gross_amount")
private BigDecimal grossAmount; // Salário Bruto (Base + Subsídio)
}
@@ -0,0 +1,36 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
/**
* Entidade que representa um nível (step) dentro de um grau salarial.
*/
@Entity
@Table(name = "salary_step", indexes = {
@Index(name = "idx_salary_step_grade", columnList = "grade_id"),
@Index(name = "idx_salary_step_number", columnList = "grade_id,step_number")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SalaryStep extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grade_id", nullable = false)
private SalaryGrade grade;
@Column(nullable = false, name = "step_number")
private Integer stepNumber; // Número do nível (1, 2, 3, etc.)
@OneToMany(mappedBy = "step", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<SalaryGrid> grids = new ArrayList<>();
}
@@ -0,0 +1,49 @@
package br.gov.sigefp.rh.domain;
import br.gov.sigefp.common.domain.AuditableEntity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* Representa um escalão de imposto progressivo (ex: IRPS).
*/
@Entity
@Table(name = "tax_bracket")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class TaxBracket extends AuditableEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deduction_type_id", nullable = false)
private DeductionType deductionType;
@Column(nullable = false, precision = 19, scale = 2)
private BigDecimal lowerLimit; // Limite inferior do escalão
@Column(precision = 19, scale = 2)
private BigDecimal upperLimit; // Limite superior (null para o último escalão)
@Column(precision = 5, scale = 4)
private BigDecimal ratePercentage; // Taxa (ex: 0.15 para 15%) - Pode ser null se for valor fixo
@Column(precision = 19, scale = 2)
private BigDecimal excessDeduction; // Parcela a abater - Pode ser null se for valor fixo
@Column(name = "fixed_amount", precision = 19, scale = 2)
private BigDecimal fixedAmount; // Valor fixo (para impostos não progressivos como Imposto Democracia)
@Column(nullable = false, name = "valid_from")
private LocalDate validFrom;
@Column(name = "valid_to")
private LocalDate validTo;
}
@@ -0,0 +1,32 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.Absence;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface AbsenceRepository extends JpaRepository<Absence, UUID> {
@Query("SELECT a FROM Absence a WHERE a.agent.id = :agentId " +
"AND a.startDate <= :endDate AND a.endDate >= :startDate")
List<Absence> findByAgentIdAndDateRange(@Param("agentId") UUID agentId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
@Query("SELECT a FROM Absence a WHERE a.agent.id IN :agentIds " +
"AND a.startDate <= :endDate AND a.endDate >= :startDate")
List<Absence> findByAgentIdInAndDateRange(@Param("agentIds") List<UUID> agentIds,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
List<Absence> findByAgentId(UUID agentId);
org.springframework.data.domain.Page<Absence> findByAgentId(UUID agentId,
org.springframework.data.domain.Pageable pageable);
}
@@ -0,0 +1,22 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.Agent;
import br.gov.sigefp.rh.domain.AgentBankAccount;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface AgentBankAccountRepository extends JpaRepository<AgentBankAccount, UUID> {
List<AgentBankAccount> findByAgentId(UUID agentId);
Optional<AgentBankAccount> findByAgentIdAndIsPrimaryTrue(UUID agentId);
Optional<AgentBankAccount> findByAgentAndIsPrimaryTrue(Agent agent);
List<AgentBankAccount> findAllByAgentIdInAndIsPrimaryTrue(List<UUID> agentIds);
}
@@ -0,0 +1,23 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.Agent;
import br.gov.sigefp.rh.domain.AgentContract;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface AgentContractRepository extends JpaRepository<AgentContract, UUID> {
List<AgentContract> findByAgentId(UUID agentId);
List<AgentContract> findByAgentIdAndIsActiveTrue(UUID agentId);
Optional<AgentContract> findByAgentAndIsActiveTrue(Agent agent);
List<AgentContract> findByAgentIdIn(List<UUID> agentIds);
List<AgentContract> findAllByAgentIdInAndIsActiveTrue(List<UUID> agentIds);
}
@@ -0,0 +1,45 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.Agent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface AgentRepository extends JpaRepository<Agent, UUID>, JpaSpecificationExecutor<Agent> {
Optional<Agent> findByMatricula(String matricula);
Optional<Agent> findByNif(String nif);
Optional<Agent> findByBiNumber(String biNumber);
boolean existsByMatricula(String matricula);
boolean existsByNif(String nif);
boolean existsByBiNumber(String biNumber);
java.util.List<Agent> findByStatus(String status);
org.springframework.data.domain.Page<Agent> findByFullNameContainingIgnoreCaseOrMatriculaContainingIgnoreCase(
String fullName, String matricula, org.springframework.data.domain.Pageable pageable);
long countByStatus(String status);
boolean existsBySalaryCategory(UUID salaryCategory);
boolean existsBySalaryGrade(UUID salaryGrade);
boolean existsBySalaryStep(UUID salaryStep);
@org.springframework.data.jpa.repository.Query("SELECT a.status, COUNT(a) FROM Agent a GROUP BY a.status")
java.util.List<Object[]> countAgentsByStatus();
@org.springframework.data.jpa.repository.Query("SELECT a.status, COUNT(a) FROM Agent a WHERE (:spec IS NULL OR a.status IS NOT NULL) GROUP BY a.status")
java.util.List<Object[]> countAgentsByStatusWithSpecification(
@org.springframework.data.repository.query.Param("spec") org.springframework.data.jpa.domain.Specification<Agent> spec);
}
@@ -0,0 +1,16 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.AgentStatusHistory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface AgentStatusHistoryRepository extends JpaRepository<AgentStatusHistory, UUID> {
List<AgentStatusHistory> findByAgentIdOrderByChangedAtDesc(UUID agentId);
void deleteByAgentId(UUID agentId);
}
@@ -0,0 +1,27 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.AttendanceRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface AttendanceRecordRepository extends JpaRepository<AttendanceRecord, UUID> {
List<AttendanceRecord> findBySheetId(UUID sheetId);
@Query("SELECT r FROM AttendanceRecord r WHERE r.agent.id = :agentId AND r.date BETWEEN :startDate AND :endDate")
List<AttendanceRecord> findByAgentIdAndDateRange(UUID agentId, LocalDate startDate, LocalDate endDate);
@Query("SELECT r FROM AttendanceRecord r WHERE r.agent.id IN :agentIds AND r.date BETWEEN :startDate AND :endDate")
List<AttendanceRecord> findByAgentIdInAndDateRange(List<UUID> agentIds, LocalDate startDate, LocalDate endDate);
@Modifying
@Query("DELETE FROM AttendanceRecord r WHERE r.sheet.id = :sheetId")
void deleteBySheetId(UUID sheetId);
}
@@ -0,0 +1,16 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.CareerEvent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface CareerEventRepository extends JpaRepository<CareerEvent, UUID> {
List<CareerEvent> findByAgentIdOrderByEffectiveDateDesc(UUID agentId);
List<CareerEvent> findByAgentIdOrderByEffectiveDateAsc(UUID agentId);
void deleteByAgentId(UUID agentId);
}
@@ -0,0 +1,15 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.CareerRegime;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface CareerRegimeRepository extends JpaRepository<CareerRegime, UUID> {
Optional<CareerRegime> findByCode(String code);
boolean existsByCode(String code);
}
@@ -0,0 +1,13 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.DeductionType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface DeductionTypeRepository extends JpaRepository<DeductionType, UUID> {
Optional<DeductionType> findByCode(String code);
}
@@ -0,0 +1,13 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.EarningType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface EarningTypeRepository extends JpaRepository<EarningType, UUID> {
Optional<EarningType> findByCode(String code);
}
@@ -0,0 +1,17 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.FamilyAllowanceTable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
import java.util.Optional;
import java.util.UUID;
public interface FamilyAllowanceTableRepository extends JpaRepository<FamilyAllowanceTable, UUID> {
@Query("SELECT f FROM FamilyAllowanceTable f WHERE f.dependentsCount = :count AND f.validFrom <= :date AND (f.validTo IS NULL OR f.validTo >= :date)")
Optional<FamilyAllowanceTable> findByDependentsAndDate(@Param("count") Integer count,
@Param("date") LocalDate date);
}
@@ -0,0 +1,22 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.GlobalDeductionRule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface GlobalDeductionRuleRepository extends JpaRepository<GlobalDeductionRule, UUID> {
@Query("SELECT r FROM GlobalDeductionRule r WHERE r.active = true AND r.validFrom <= :date AND (r.validTo IS NULL OR r.validTo >= :date)")
List<GlobalDeductionRule> findActiveRules(@Param("date") LocalDate date);
@Query("SELECT r FROM GlobalDeductionRule r WHERE r.deductionType.id = :typeId AND r.active = true AND (r.validTo IS NULL OR r.validTo >= :start) AND r.validFrom <= :end")
List<GlobalDeductionRule> findOverlappingRules(@Param("typeId") UUID typeId, @Param("start") LocalDate start,
@Param("end") LocalDate end);
}
@@ -0,0 +1,24 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.AttendanceSheetStatus;
import br.gov.sigefp.rh.domain.MonthlyAttendanceSheet;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface MonthlyAttendanceSheetRepository extends JpaRepository<MonthlyAttendanceSheet, UUID> {
Optional<MonthlyAttendanceSheet> findByOrgUnitIdAndMonthAndYear(UUID orgUnitId, Integer month, Integer year);
List<MonthlyAttendanceSheet> findByMonthAndYear(Integer month, Integer year);
List<MonthlyAttendanceSheet> findByStatus(AttendanceSheetStatus status);
@Query("SELECT s FROM MonthlyAttendanceSheet s WHERE s.orgUnitId = :orgUnitId ORDER BY s.year DESC, s.month DESC")
List<MonthlyAttendanceSheet> findByOrgUnitId(UUID orgUnitId);
}
@@ -0,0 +1,15 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.PayrollItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface PayrollItemRepository extends JpaRepository<PayrollItem, UUID> {
List<PayrollItem> findByPayrollRunId(UUID payrollRunId);
}
@@ -0,0 +1,17 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.PayrollPeriod;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface PayrollPeriodRepository extends JpaRepository<PayrollPeriod, UUID> {
Optional<PayrollPeriod> findByFiscalYearAndMonth(Integer fiscalYear, Integer month);
boolean existsByFiscalYearAndMonth(Integer fiscalYear, Integer month);
}
@@ -0,0 +1,20 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.PayrollRun;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface PayrollRunRepository extends JpaRepository<PayrollRun, UUID> {
List<PayrollRun> findByPeriodId(UUID periodId);
@Query("SELECT pr FROM PayrollRun pr WHERE pr.period.id = :periodId AND pr.ministry = :ministryId")
List<PayrollRun> findByPeriodIdAndMinistry(@Param("periodId") UUID periodId, @Param("ministryId") UUID ministryId);
}
@@ -0,0 +1,16 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.PerformanceEvaluation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
@Repository
public interface PerformanceEvaluationRepository extends JpaRepository<PerformanceEvaluation, UUID> {
List<PerformanceEvaluation> findByAgentIdOrderByReferenceYearDesc(UUID agentId);
List<PerformanceEvaluation> findByAgentIdAndReferenceYearBetweenOrderByReferenceYearDesc(UUID agentId,
Integer startYear, Integer endYear);
}
@@ -0,0 +1,17 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.SalaryCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface SalaryCategoryRepository extends JpaRepository<SalaryCategory, UUID> {
boolean existsByCode(String code);
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = { "regime" })
java.util.List<SalaryCategory> findAll();
java.util.Optional<SalaryCategory> findByCode(String code);
}
@@ -0,0 +1,18 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.SalaryGrade;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
import java.util.Optional; // Added
@Repository
public interface SalaryGradeRepository extends JpaRepository<SalaryGrade, UUID> {
@org.springframework.data.jpa.repository.EntityGraph(attributePaths = { "category", "steps" })
List<SalaryGrade> findByCategoryId(UUID categoryId);
Optional<SalaryGrade> findByCode(String code);
}
@@ -0,0 +1,20 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.SalaryGrid;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface SalaryGridRepository extends JpaRepository<SalaryGrid, UUID> {
@Query("SELECT g FROM SalaryGrid g WHERE g.step.id = :stepId AND :date BETWEEN g.validFrom AND COALESCE(g.validTo, '2999-12-31')")
SalaryGrid findByStepIdAndDate(@Param("stepId") UUID stepId, @Param("date") LocalDate date);
List<SalaryGrid> findByStepId(UUID stepId);
}
@@ -0,0 +1,18 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.SalaryStep;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.UUID;
import br.gov.sigefp.rh.domain.SalaryGrade;
import java.util.Optional;
@Repository
public interface SalaryStepRepository extends JpaRepository<SalaryStep, UUID> {
List<SalaryStep> findByGradeIdOrderByStepNumber(UUID gradeId);
Optional<SalaryStep> findByGradeAndStepNumber(SalaryGrade grade, Integer stepNumber);
}
@@ -0,0 +1,25 @@
package br.gov.sigefp.rh.repository;
import br.gov.sigefp.rh.domain.TaxBracket;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
@Repository
public interface TaxBracketRepository extends JpaRepository<TaxBracket, UUID> {
@Query("SELECT b FROM TaxBracket b WHERE b.validFrom <= :date AND (b.validTo IS NULL OR b.validTo >= :date) ORDER BY b.lowerLimit ASC")
List<TaxBracket> findActiveBrackets(@Param("date") LocalDate date);
@Query("SELECT b FROM TaxBracket b WHERE b.deductionType.id = :typeId AND (b.validTo IS NULL OR b.validTo >= :start) AND b.validFrom <= :end "
+
"AND b.lowerLimit < :upper AND (b.upperLimit IS NULL OR b.upperLimit > :lower)")
List<TaxBracket> findOverlappingBrackets(@Param("typeId") UUID typeId,
@Param("start") LocalDate start, @Param("end") LocalDate end,
@Param("lower") java.math.BigDecimal lower, @Param("upper") java.math.BigDecimal upper);
}
@@ -0,0 +1,88 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.rh.api.dto.AbsenceDTO;
import br.gov.sigefp.rh.api.dto.CreateAbsenceDTO;
import br.gov.sigefp.rh.domain.Absence;
import br.gov.sigefp.rh.domain.Agent;
import br.gov.sigefp.rh.repository.AbsenceRepository;
import br.gov.sigefp.rh.repository.AgentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional
public class AbsenceService {
private final AbsenceRepository absenceRepository;
private final AgentRepository agentRepository;
public AbsenceDTO create(CreateAbsenceDTO dto) {
Agent agent = agentRepository.findById(dto.getAgentId())
.orElseThrow(() -> new IllegalArgumentException("Agent not found"));
if (dto.getEndDate().isBefore(dto.getStartDate())) {
throw new IllegalArgumentException("Data final deve ser posterior ou igual a data inicial");
}
// Calculate days inclusive
long daysDiff = ChronoUnit.DAYS.between(dto.getStartDate(), dto.getEndDate()) + 1;
Absence absence = Absence.builder()
.agent(agent)
.startDate(dto.getStartDate())
.endDate(dto.getEndDate())
.days((int) daysDiff)
.reason(dto.getReason())
.justified(dto.isJustified())
.build();
Absence saved = absenceRepository.save(absence);
return toDTO(saved);
}
@Transactional(readOnly = true)
public Page<AbsenceDTO> findByAgent(UUID agentId, Pageable pageable) {
return absenceRepository.findByAgentId(agentId, pageable)
.map(this::toDTO);
}
@Transactional(readOnly = true)
public List<AbsenceDTO> findAllByAgent(UUID agentId) {
return absenceRepository.findByAgentId(agentId).stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
public void delete(UUID id) {
Absence absence = absenceRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Absence not found"));
if (absence.getDeductedInPayrollRunId() != null) {
throw new BusinessException("Cannot delete absence already processed in payroll", "ABSENCE_PROCESSED",
org.springframework.http.HttpStatus.BAD_REQUEST);
}
absenceRepository.delete(absence);
}
private AbsenceDTO toDTO(Absence absence) {
return AbsenceDTO.builder()
.id(absence.getId())
.agentId(absence.getAgent().getId())
.agentName(absence.getAgent().getFullName())
.startDate(absence.getStartDate())
.endDate(absence.getEndDate())
.days(absence.getDays())
.reason(absence.getReason())
.justified(absence.isJustified())
.build();
}
}
@@ -0,0 +1,60 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.domain.Agent;
import br.gov.sigefp.rh.domain.AgentBankAccount;
import br.gov.sigefp.rh.repository.AgentBankAccountRepository;
import br.gov.sigefp.rh.repository.AgentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* Serviço para gestão de contas bancárias de agentes.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class AgentBankAccountService {
private final AgentBankAccountRepository repository;
private final AgentRepository agentRepository;
public AgentBankAccount save(AgentBankAccount account) {
Agent agent = agentRepository.findById(account.getAgent().getId())
.orElseThrow(
() -> new IllegalArgumentException("Agente não encontrado: " + account.getAgent().getId()));
account.setAgent(agent);
handlePrimaryStatus(account);
return repository.save(account);
}
public AgentBankAccount update(AgentBankAccount account) {
AgentBankAccount existing = repository.findById(account.getId())
.orElseThrow(() -> new IllegalArgumentException("Conta bancária não encontrada: " + account.getId()));
existing.setBank(account.getBank());
existing.setBranchCode(account.getBranchCode());
existing.setAccountNumber(account.getAccountNumber());
existing.setIban(account.getIban());
existing.setIsPrimary(account.getIsPrimary());
handlePrimaryStatus(existing);
return repository.save(existing);
}
private void handlePrimaryStatus(AgentBankAccount account) {
if (Boolean.TRUE.equals(account.getIsPrimary())) {
// Desativar outras contas primárias do mesmo agente
List<AgentBankAccount> currentAccounts = repository.findByAgentId(account.getAgent().getId());
for (AgentBankAccount existing : currentAccounts) {
if (!existing.getId().equals(account.getId()) && Boolean.TRUE.equals(existing.getIsPrimary())) {
existing.setIsPrimary(false);
repository.save(existing);
}
}
}
}
}
@@ -0,0 +1,187 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.AgentContractRepository;
import br.gov.sigefp.rh.repository.AgentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
/**
* Serviço para gestão de contratos de agentes com sincronização automática.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class AgentContractService {
private final AgentContractRepository contractRepository;
private final AgentRepository agentRepository;
private final CareerEventService careerEventService;
/**
* Salva um novo contrato, desativando os anteriores e atualizando o agente.
*/
/**
* Atualiza um contrato existente e sincroniza dados se for o contrato ativo.
*/
public AgentContract updateContract(AgentContract contract) {
AgentContract existing = contractRepository.findById(contract.getId())
.orElseThrow(() -> new IllegalArgumentException("Contrato não encontrado: " + contract.getId()));
Agent agent = agentRepository.findById(contract.getAgent().getId())
.orElseThrow(
() -> new IllegalArgumentException("Agente não encontrado: " + contract.getAgent().getId()));
// Validar sobreposição
validateContractOverlap(agent, existing);
// Atualizar campos permitidos
existing.setContractType(contract.getContractType());
existing.setStartDate(contract.getStartDate());
existing.setEndDate(contract.getEndDate());
existing.setWeeklyHours(contract.getWeeklyHours());
existing.setOrgUnit(contract.getOrgUnit());
existing.setPosition(contract.getPosition());
existing.setSalaryCategory(contract.getSalaryCategory());
existing.setSalaryGrade(contract.getSalaryGrade());
existing.setSalaryStep(contract.getSalaryStep());
existing.setLegalActReference(contract.getLegalActReference());
AgentContract saved = contractRepository.save(existing);
// Se o contrato for o ativo, sincronizar os dados no Agente
if (Boolean.TRUE.equals(saved.getIsActive())) {
syncCareerData(agent, saved);
String newAppointmentType = mapContractToAppointmentType(saved.getContractType());
if (newAppointmentType != null) {
agent.setAppointmentType(newAppointmentType);
}
agentRepository.save(agent);
// Registrar Evento de Retificação
careerEventService.recordEvent(agent, CareerEventType.RETIFICACAO,
"Retificação de Ato Administrativo: " + saved.getLegalActReference(),
saved.getLegalActReference(), saved.getStartDate(), LocalDate.now(),
saved.getSalaryCategory(), saved.getSalaryGrade(), saved.getSalaryStep(),
saved.getOrgUnit(), saved.getPosition(),
null, null, null, null, null);
}
return saved;
}
public AgentContract saveContract(AgentContract contract) {
Agent agent = agentRepository.findById(contract.getAgent().getId())
.orElseThrow(
() -> new IllegalArgumentException("Agente não encontrado: " + contract.getAgent().getId()));
// Validar sobreposição antes de desativar
validateContractOverlap(agent, contract);
// 1. Desativar contratos ativos anteriores
List<AgentContract> activeContracts = contractRepository.findByAgentId(agent.getId());
for (AgentContract oldContract : activeContracts) {
if (Boolean.TRUE.equals(oldContract.getIsActive())) {
oldContract.setIsActive(false);
contractRepository.save(oldContract);
}
}
// 2. Definir o novo contrato como ativo
contract.setIsActive(true);
contract.setAgent(agent);
AgentContract savedContract = contractRepository.save(contract);
// 3. Sincronizar todos os dados de carreira do Contrato para o Agente
syncCareerData(agent, savedContract);
// 4. Ativação automática se a data de início for válida
if (!savedContract.getStartDate().isAfter(LocalDate.now())) {
agent.setStatus("ACTIVE");
}
// 5. Sincronizar appointmentType do Agente
String newAppointmentType = mapContractToAppointmentType(contract.getContractType());
if (newAppointmentType != null) {
agent.setAppointmentType(newAppointmentType);
}
agentRepository.save(agent);
// 6. Registar Evento de Carreira Automático via Serviço Centralizado
recordAutomaticCareerEvent(agent, savedContract);
return savedContract;
}
private void syncCareerData(Agent agent, AgentContract contract) {
if (contract.getOrgUnit() != null)
agent.setOrgUnit(contract.getOrgUnit());
if (contract.getPosition() != null)
agent.setPosition(contract.getPosition());
if (contract.getSalaryCategory() != null)
agent.setSalaryCategory(contract.getSalaryCategory());
if (contract.getSalaryGrade() != null)
agent.setSalaryGrade(contract.getSalaryGrade());
if (contract.getSalaryStep() != null)
agent.setSalaryStep(contract.getSalaryStep());
}
private String mapContractToAppointmentType(String contractType) {
if (contractType == null)
return null;
return switch (contractType.toUpperCase()) {
case "DEFINITIVA", "PERMANENT" -> "DEFINITIVA";
case "PROVISORIA", "PROBATION" -> "PROVISORIA";
case "FIXED_TERM", "CONTRATO", "TERMO" -> "CONTRATO_TERMO";
case "PROVIMENTO" -> "CONTRATO_PROVIMENTO";
default -> null;
};
}
private void recordAutomaticCareerEvent(Agent agent, AgentContract contract) {
CareerEventType eventType = switch (contract.getContractType().toUpperCase()) {
case "DEFINITIVA" -> CareerEventType.NOMEACAO_DEFINITIVA;
case "PROVISORIA" -> CareerEventType.NOMEACAO_PROVISORIA;
default -> CareerEventType.ADMISSAO;
};
careerEventService.recordEvent(agent, eventType,
"Ato Administrativo: " + contract.getLegalActReference() + " (" + contract.getContractType() + ")",
contract.getLegalActReference(), contract.getStartDate(), LocalDate.now(),
contract.getSalaryCategory(), contract.getSalaryGrade(), contract.getSalaryStep(),
contract.getOrgUnit(), contract.getPosition(),
null, null, null, null, null);
}
private void validateContractOverlap(Agent agent, AgentContract newContract) {
List<AgentContract> existingContracts = contractRepository.findByAgentId(agent.getId());
for (AgentContract existing : existingContracts) {
// Skip if it's the same contract (update case)
if (newContract.getId() != null && newContract.getId().equals(existing.getId())) {
continue;
}
// Logic: Overlap if (StartA <= EndB) and (EndA >= StartB)
// Handle null endDate as "Infinity"
LocalDate startA = existing.getStartDate();
LocalDate endA = existing.getEndDate();
LocalDate startB = newContract.getStartDate();
LocalDate endB = newContract.getEndDate();
boolean startA_le_endB = (endB == null) || !startA.isAfter(endB);
boolean endA_ge_startB = (endA == null) || !endA.isBefore(startB);
if (startA_le_endB && endA_ge_startB) {
throw new IllegalArgumentException(
String.format("Sobreposição detectada com contrato existente (ID: %s, Início: %s, Fim: %s)",
existing.getId(), startA, endA));
}
}
}
}
@@ -0,0 +1,220 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.api.dto.AgentImportDTO;
import br.gov.sigefp.rh.api.dto.AgentImportResultDTO;
import br.gov.sigefp.rh.domain.Agent;
import br.gov.sigefp.rh.domain.AgentBankAccount;
import br.gov.sigefp.rh.domain.AgentContract;
import br.gov.sigefp.rh.repository.AgentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
@Service
@Slf4j
@RequiredArgsConstructor
public class AgentImportService {
private final AgentRepository agentRepository;
private final AgentContractService contractService;
// Removed AgentService to avoid nested @PreAuthorize issues
public AgentImportResultDTO importAgents(MultipartFile file) {
AgentImportResultDTO result = AgentImportResultDTO.builder()
.errors(new ArrayList<>())
.build();
try (InputStream is = file.getInputStream();
Workbook workbook = new XSSFWorkbook(is)) {
Sheet sheet = workbook.getSheetAt(0);
int rowIdx = 0;
for (Row row : sheet) {
if (rowIdx == 0) { // Skip Header
rowIdx++;
continue;
}
// Skip empty rows
if (row.getCell(0) == null || row.getCell(0).getStringCellValue().trim().isEmpty()) {
continue;
}
try {
processRow(row);
result.setSuccessCount(result.getSuccessCount() + 1);
} catch (Exception e) {
result.setFailureCount(result.getFailureCount() + 1);
String errorMsg = String.format("Linha %d: %s", rowIdx + 1, e.getMessage());
result.getErrors().add(errorMsg);
log.error("Erro importando linha {}", rowIdx + 1, e);
}
result.setTotalProcessed(result.getTotalProcessed() + 1);
rowIdx++;
}
} catch (Exception e) {
log.error("Erro ao ler arquivo Excel", e);
result.getErrors().add("Erro fatal ao ler arquivo: " + e.getMessage());
}
return result;
}
@Transactional
protected void processRow(Row row) {
// 1. Parse DTO from Excel row
AgentImportDTO dto = parseRowToDTO(row);
// 2. Determine matricula: use from Excel or default to NIF
String matricula = (dto.getMatricula() != null && !dto.getMatricula().isEmpty())
? dto.getMatricula()
: dto.getNif();
// 3. Validate unique constraints
if (agentRepository.existsByMatricula(matricula)) {
throw new IllegalArgumentException("Matrícula já existe: " + matricula);
}
if (agentRepository.existsByNif(dto.getNif())) {
throw new IllegalArgumentException("NIF já existe: " + dto.getNif());
}
if (agentRepository.existsByBiNumber(dto.getBi())) {
throw new IllegalArgumentException("BI já existe: " + dto.getBi());
}
// 4. Build Agent entity directly
LocalDate hireDate = parseDate(dto.getDataInicioContrato());
if (hireDate == null) {
hireDate = LocalDate.now();
}
// Data de nomeação: usa do Excel ou default para hireDate
LocalDate appointmentDate = parseDate(dto.getNomeacao());
if (appointmentDate == null) {
appointmentDate = hireDate;
}
Agent agent = Agent.builder()
.fullName(dto.getNome())
.nif(dto.getNif())
.biNumber(dto.getBi())
.matricula(matricula) // Use from Excel or default to NIF
.birthDate(parseDate(dto.getDataNascimento()))
.status("ACTIVE")
.hireDate(hireDate)
.posseDate(appointmentDate) // Data de nomeação/posse
.literaryQualification(dto.getHabilitacaoLiteraria())
.appointmentType("PROVISORIA")
.functionalSituation("ATIVIDADE_NO_QUADRO")
.build();
// 5. Save Agent directly (bypassing AgentService.create() @PreAuthorize)
Agent savedAgent = agentRepository.save(agent);
log.info("Agente importado com sucesso: {} (ID: {})", savedAgent.getFullName(), savedAgent.getId());
// 6. Create Contract (if present)
if (dto.getTipoContrato() != null && !dto.getTipoContrato().isEmpty()) {
AgentContract contract = AgentContract.builder()
.agent(savedAgent)
.contractType(dto.getTipoContrato())
.startDate(parseDate(dto.getDataInicioContrato()))
.endDate(parseDate(dto.getDataFimContrato()))
.isActive(true)
.build();
contractService.saveContract(contract);
}
// 7. Create Bank Account (if IBAN provided)
if (dto.getIban() != null && !dto.getIban().isEmpty()) {
AgentBankAccount bankAccount = AgentBankAccount.builder()
.agent(savedAgent)
.bank(dto.getBancoCodigo())
.iban(dto.getIban())
.isPrimary(true) // First bank account is primary
.build();
savedAgent.getBankAccounts().add(bankAccount);
agentRepository.save(savedAgent);
}
}
private AgentImportDTO parseRowToDTO(Row row) {
// Column order: NOME, NIF, MATRICULA, BI, DATA_NASCIMENTO, SEXO, ESTADO_CIVIL,
// HABILITACAO_LITERARIA, CATEGORIA_CODIGO, UNIDADE_ORG_CODIGO,
// NOMEACAO, TIPO_CONTRATO, DATA_INICIO_CONTRATO, DATA_FIM_CONTRATO,
// BANCO_CODIGO, IBAN
return AgentImportDTO.builder()
.nome(getCellString(row, 0))
.nif(getCellString(row, 1))
.matricula(getCellString(row, 2)) // MATRICULA column
.bi(getCellString(row, 3))
.dataNascimento(getCellString(row, 4))
.sexo(getCellString(row, 5))
.estadoCivil(getCellString(row, 6))
.habilitacaoLiteraria(getCellString(row, 7))
.categoriaCodigo(getCellString(row, 8))
.unidadeOrgCodigo(getCellString(row, 9))
.nomeacao(getCellString(row, 10)) // NOMEACAO (data de nomeação)
.tipoContrato(getCellString(row, 11))
.dataInicioContrato(getCellString(row, 12))
.dataFimContrato(getCellString(row, 13))
.bancoCodigo(getCellString(row, 14))
.iban(getCellString(row, 15))
.build();
}
private String getCellString(Row row, int index) {
Cell cell = row.getCell(index);
if (cell == null)
return null;
DataFormatter formatter = new DataFormatter();
return formatter.formatCellValue(cell).trim();
}
private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isEmpty())
return null;
// Formatos suportados
String[] patterns = {
"dd/MM/yyyy",
"dd-MM-yyyy",
"yyyy-MM-dd",
"d/M/yyyy",
"d-M-yyyy",
"dd/MM/yy",
"d/M/yy"
};
for (String pattern : patterns) {
try {
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
} catch (DateTimeParseException ignored) {
// Try next pattern
}
}
// Tenta interpretar número serial do Excel (dias desde 1899-12-30)
try {
double excelDate = Double.parseDouble(dateStr);
// Excel date serial number: days since 1899-12-30
return LocalDate.of(1899, 12, 30).plusDays((long) excelDate);
} catch (NumberFormatException ignored) {
// Not a number
}
throw new IllegalArgumentException("Data inválida: " + dateStr + ". Use dd/MM/yyyy");
}
}
@@ -0,0 +1,678 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.org.repository.OrgUnitRepository;
import br.gov.sigefp.org.repository.PositionRepository;
import br.gov.sigefp.rh.api.dto.*;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Serviço de aplicação para gestão de agentes.
*/
@Service
@RequiredArgsConstructor
@Transactional
@lombok.extern.slf4j.Slf4j
public class AgentService {
private final AgentRepository agentRepository;
private final OrgUnitRepository orgUnitRepository;
private final PositionRepository positionRepository;
private final AgentStatusHistoryRepository statusHistoryRepository;
private final CareerEventRepository careerEventRepository;
private final PerformanceEvaluationRepository performanceEvaluationRepository;
private final CareerEventService careerEventService;
private final AgentContractRepository agentContractRepository;
private final AgentBankAccountRepository agentBankAccountRepository;
@org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_ADMIN')")
public AgentDTO create(AgentDTO dto) {
if (agentRepository.existsByMatricula(dto.getMatricula())) {
throw new IllegalArgumentException("Matrícula já existe: " + dto.getMatricula());
}
if (agentRepository.existsByNif(dto.getNif())) {
throw new IllegalArgumentException("NIF já existe: " + dto.getNif());
}
if (agentRepository.existsByBiNumber(dto.getBiNumber())) {
throw new IllegalArgumentException("Número de BI já existe: " + dto.getBiNumber());
}
// Validações cruzadas
if (dto.getOrgUnit() != null && !orgUnitRepository.existsById(dto.getOrgUnit())) {
throw new IllegalArgumentException("Unidade organizacional não encontrada: " + dto.getOrgUnit());
}
if (dto.getPosition() != null && !positionRepository.existsById(dto.getPosition())) {
throw new IllegalArgumentException("Posição não encontrada: " + dto.getPosition());
}
// P17: Validar Habilitação Literária
validateLiteraryQualification(dto.getSalaryCategory(), dto.getLiteraryQualification());
Agent agent = Agent.builder()
.matricula(dto.getMatricula())
.nif(dto.getNif())
.biNumber(dto.getBiNumber())
.fullName(dto.getFullName())
.birthDate(dto.getBirthDate())
.hireDate(dto.getHireDate())
.posseDate(dto.getPosseDate())
.terminationDate(dto.getTerminationDate())
.appointmentType(dto.getAppointmentType() != null ? dto.getAppointmentType() : "PROVISORIA")
.functionalSituation(
dto.getFunctionalSituation() != null ? dto.getFunctionalSituation() : "ATIVIDADE_NO_QUADRO")
.status(dto.getStatus() != null ? dto.getStatus() : "REGISTERED")
.eligibleDependentsCount(dto.getEligibleDependentsCount())
.literaryQualification(dto.getLiteraryQualification())
.salaryCategory(dto.getSalaryCategory())
.salaryGrade(dto.getSalaryGrade())
.salaryStep(dto.getSalaryStep())
.orgUnit(dto.getOrgUnit())
.position(dto.getPosition())
.nationality(dto.getNationality())
.phone(dto.getPhone())
.email(dto.getEmail())
.address(dto.getAddress())
.build();
Agent saved = agentRepository.save(agent);
careerEventService.recordSimpleEvent(saved, CareerEventType.ADMISSAO, "Admissão inicial no sistema", null,
saved.getHireDate());
return toDTO(saved);
}
@org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_ADMIN')")
public AgentDTO update(UUID id, AgentDTO dto) {
// P17: Validar Habilitação Literária (em update também)
validateLiteraryQualification(dto.getSalaryCategory(), dto.getLiteraryQualification());
Agent agent = agentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Agente não encontrado: " + id));
// Capture previous state for history
String previousStatus = agent.getStatus();
String previousSituation = agent.getFunctionalSituation();
UUID previousCategory = agent.getSalaryCategory();
UUID previousGrade = agent.getSalaryGrade();
UUID previousStep = agent.getSalaryStep();
UUID previousOrgUnit = agent.getOrgUnit();
UUID previousPosition = agent.getPosition();
StringBuilder changeLog = new StringBuilder();
// Detect Changes for ChangeLog
detectAndApplyChanges(agent, dto, changeLog);
boolean statusChanged = dto.getStatus() != null && !dto.getStatus().equals(previousStatus);
boolean situationChanged = dto.getFunctionalSituation() != null
&& !dto.getFunctionalSituation().equals(previousSituation);
boolean salaryChanged = isSalaryChanged(agent, dto);
boolean orgChanged = isOrgChanged(agent, dto);
CareerEventType careerEventType = null;
boolean careerImpact = false;
if (statusChanged || situationChanged || salaryChanged || orgChanged) {
careerImpact = true;
careerEventType = determineEventType(dto, agent, situationChanged, salaryChanged, orgChanged);
if (careerEventType == CareerEventType.PROMOCAO) {
validatePromotion(agent);
}
}
// Apply changes to entity
applyUpdates(agent, dto);
// Record History
if (changeLog.length() > 0 || careerImpact) {
AgentStatusHistory history = AgentStatusHistory.builder()
.agent(agent)
.previousStatus(previousStatus)
.newStatus(agent.getStatus())
.previousFunctionalSituation(previousSituation)
.newFunctionalSituation(agent.getFunctionalSituation())
.eventType(careerImpact && careerEventType != null ? careerEventType.name() : "EDIT")
.reason(dto.getStatusChangeReason())
.changeLog(changeLog.length() > 0 ? changeLog.toString() : "Atualização cadastral.")
.changedAt(LocalDateTime.now())
.changedBy("system")
.build();
statusHistoryRepository.save(history);
if (careerImpact && careerEventType != null) {
careerEventService.recordEvent(agent, careerEventType, dto.getStatusChangeReason(),
dto.getEventDocumentRef(),
dto.getEventEffectiveDate(), dto.getEventPublicationDate(),
dto.getSalaryCategory(), dto.getSalaryGrade(), dto.getSalaryStep(),
dto.getOrgUnit(), dto.getPosition(),
previousCategory, previousGrade, previousStep,
previousOrgUnit, previousPosition);
}
}
Agent saved = agentRepository.save(agent);
return toDTO(saved);
}
@org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_ADMIN')")
public void delete(UUID id) {
Agent agent = agentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Agente não encontrado: " + id));
log.info("Excluindo agente: {} (ID: {})", agent.getFullName(), agent.getId());
// Excluir dados relacionados primeiro (para evitar problemas de FK)
// Os contratos, contas bancárias e histórico são deletados em cascata via
// orphanRemoval
// Mas eventos de carreira e histórico de status precisam ser excluídos
// explicitamente
// Excluir eventos de carreira
careerEventRepository.deleteByAgentId(id);
// Excluir histórico de status
statusHistoryRepository.deleteByAgentId(id);
// Por fim, excluir o agente (contratos e contas bancárias são excluídos em
// cascata)
agentRepository.delete(agent);
log.info("Agente excluído com sucesso: {}", id);
}
private void detectAndApplyChanges(Agent agent, AgentDTO dto, StringBuilder log) {
compareAndLog(log, "Nome", agent.getFullName(), dto.getFullName());
compareAndLog(log, "NIF", agent.getNif(), dto.getNif());
compareAndLog(log, "BI", agent.getBiNumber(), dto.getBiNumber());
compareAndLog(log, "Email", agent.getEmail(), dto.getEmail());
compareAndLog(log, "Telefone", agent.getPhone(), dto.getPhone());
compareAndLog(log, "Endereço", agent.getAddress(), dto.getAddress());
compareAndLog(log, "Situação Funcional", agent.getFunctionalSituation(), dto.getFunctionalSituation());
compareAndLog(log, "Status", agent.getStatus(), dto.getStatus());
compareAndLog(log, "Nº Dependentes", agent.getEligibleDependentsCount(), dto.getEligibleDependentsCount());
compareAndLog(log, "Habilitação Literária", agent.getLiteraryQualification(), dto.getLiteraryQualification());
if (dto.getOrgUnit() != null && !Objects.equals(dto.getOrgUnit(), agent.getOrgUnit())) {
log.append("Unidade Orgânica: [").append(agent.getOrgUnit()).append("] -> [").append(dto.getOrgUnit())
.append("]. ");
}
if (dto.getPosition() != null && !Objects.equals(dto.getPosition(), agent.getPosition())) {
log.append("Cargo/Posição: [").append(agent.getPosition()).append("] -> [").append(dto.getPosition())
.append("]. ");
}
if (isSalaryChanged(agent, dto)) {
log.append("Estrutura salarial/carreira alterada. ");
}
}
private void compareAndLog(StringBuilder log, String fieldName, Object oldValue, Object newValue) {
if (newValue != null && !Objects.equals(oldValue, newValue)) {
String oldStr = oldValue != null ? oldValue.toString() : "N/A";
String newStr = newValue.toString();
log.append(fieldName).append(": [").append(oldStr).append("] -> [").append(newStr).append("]. ");
}
}
private void applyUpdates(Agent agent, AgentDTO dto) {
if (dto.getMatricula() != null)
agent.setMatricula(dto.getMatricula());
if (dto.getNif() != null)
agent.setNif(dto.getNif());
if (dto.getBiNumber() != null)
agent.setBiNumber(dto.getBiNumber());
if (dto.getFullName() != null)
agent.setFullName(dto.getFullName());
if (dto.getBirthDate() != null)
agent.setBirthDate(dto.getBirthDate());
if (dto.getHireDate() != null)
agent.setHireDate(dto.getHireDate());
if (dto.getPosseDate() != null)
agent.setPosseDate(dto.getPosseDate());
if (dto.getTerminationDate() != null)
agent.setTerminationDate(dto.getTerminationDate());
if (dto.getAppointmentType() != null)
agent.setAppointmentType(dto.getAppointmentType());
if (dto.getFunctionalSituation() != null)
agent.setFunctionalSituation(dto.getFunctionalSituation());
if (dto.getEligibleDependentsCount() != null)
agent.setEligibleDependentsCount(dto.getEligibleDependentsCount());
if (dto.getStatus() != null)
agent.setStatus(dto.getStatus());
if (dto.getOrgUnit() != null)
agent.setOrgUnit(dto.getOrgUnit());
if (dto.getPosition() != null)
agent.setPosition(dto.getPosition());
if (dto.getNationality() != null)
agent.setNationality(dto.getNationality());
if (dto.getPhone() != null)
agent.setPhone(dto.getPhone());
if (dto.getEmail() != null)
agent.setEmail(dto.getEmail());
if (dto.getAddress() != null)
agent.setAddress(dto.getAddress());
if (dto.getLiteraryQualification() != null)
agent.setLiteraryQualification(dto.getLiteraryQualification());
if (dto.getSalaryCategory() != null)
agent.setSalaryCategory(dto.getSalaryCategory());
if (dto.getSalaryGrade() != null)
agent.setSalaryGrade(dto.getSalaryGrade());
if (dto.getSalaryStep() != null)
agent.setSalaryStep(dto.getSalaryStep());
}
private boolean isSalaryChanged(Agent agent, AgentDTO dto) {
return (dto.getSalaryCategory() != null && !Objects.equals(dto.getSalaryCategory(), agent.getSalaryCategory()))
|| (dto.getSalaryGrade() != null && !Objects.equals(dto.getSalaryGrade(), agent.getSalaryGrade()))
|| (dto.getSalaryStep() != null && !Objects.equals(dto.getSalaryStep(), agent.getSalaryStep()));
}
private boolean isOrgChanged(Agent agent, AgentDTO dto) {
return (dto.getOrgUnit() != null && !Objects.equals(dto.getOrgUnit(), agent.getOrgUnit()))
|| (dto.getPosition() != null && !Objects.equals(dto.getPosition(), agent.getPosition()));
}
private CareerEventType determineEventType(AgentDTO dto, Agent agent, boolean situationChanged,
boolean salaryChanged, boolean orgChanged) {
if (salaryChanged) {
if (dto.getSalaryCategory() != null
&& !Objects.equals(dto.getSalaryCategory(), agent.getSalaryCategory())) {
return CareerEventType.PROMOCAO;
}
return CareerEventType.PROGRESSAO;
}
if (situationChanged) {
return switch (dto.getFunctionalSituation()) {
case "SUBSTITUICAO" -> CareerEventType.SUBSTITUICAO;
case "TRANSFERENCIA" -> CareerEventType.TRANSFERENCIA;
case "COMISSAO_SERVICO" -> CareerEventType.COMISSAO_SERVICO;
case "RECLASSIFICACAO" -> CareerEventType.RECLASSIFICACAO;
case "DEFINITIVA" -> CareerEventType.NOMEACAO_DEFINITIVA;
default -> CareerEventType.TRANSFERENCIA; // Fallback para outros movimentos
};
}
if (orgChanged)
return CareerEventType.TRANSFERENCIA;
return CareerEventType.PROGRESSAO; // Default
}
@Transactional(readOnly = true)
public AgentDTO findById(UUID id) {
Agent agent = agentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Agente não encontrado: " + id));
return toDTO(agent);
}
@Transactional(readOnly = true)
public Page<AgentDTO> findAll(Pageable pageable) {
return agentRepository.findAll(pageable).map(this::toDTO);
}
@Transactional(readOnly = true)
public Page<AgentDTO> search(String query, Pageable pageable) {
if (query == null || query.isBlank()) {
return findAll(pageable);
}
return agentRepository.findByFullNameContainingIgnoreCaseOrMatriculaContainingIgnoreCase(
query, query, pageable).map(this::toDTO);
}
@Transactional(readOnly = true)
public Page<AgentDTO> findAllWithFilters(
String query,
String status,
UUID ministryId,
UUID orgUnitId,
UUID positionId,
String functionalSituation,
String appointmentType,
Pageable pageable) {
Specification<Agent> spec = buildSpecification(query, status, ministryId, orgUnitId, positionId,
functionalSituation, appointmentType);
Page<Agent> agentsPage = agentRepository.findAll(spec, pageable);
// Otimização N+1: Carregar todos os contratos ativos e contas bancárias
// principais em lote para os agentes da página
List<UUID> agentIds = agentsPage.getContent().stream().map(Agent::getId).collect(Collectors.toList());
if (!agentIds.isEmpty()) {
// Isso aciona o carregamento das coleções para a sessão atual (Hibernate Batch
// Fetching deve estar configurado no yml ou na entidade)
// Como as relações estão mapeadas com @OneToMany, o Hibernate pode usar
// batch-size se configurado.
// Alternativamente, podemos fazer uma query explícita para "aquecer" o cache de
// primeiro nível.
agentContractRepository.findAllByAgentIdInAndIsActiveTrue(agentIds);
agentBankAccountRepository.findAllByAgentIdInAndIsPrimaryTrue(agentIds);
}
return agentsPage.map(this::toDTO);
}
@Transactional(readOnly = true)
public br.gov.sigefp.rh.api.dto.AgentStatsDTO getStats(
String query,
String status,
UUID ministryId,
UUID orgUnitId,
UUID positionId,
String functionalSituation,
String appointmentType) {
log.info("Calculando estatísticas otimizadas com filtros - Ministério: {}, OrgUnit: {}, Query: {}", ministryId,
orgUnitId,
query);
Specification<Agent> baseSpec = buildSpecification(query, null, ministryId, orgUnitId, positionId,
functionalSituation, appointmentType);
// Usar a nova consulta agregada para obter todos os status de uma vez
// Nota: Infelizmente a Specification não é trivial de passar para uma Query
// customizada com GROUP BY
// sem usar CriteriaBuilder complexo. Para manter a performance, vamos usar o
// repositório para o total
// e otimizar as consultas individuais se a especificação for complexa, ou usar
// uma abordagem simplificada
// se possível.
// Por enquanto, vamos reduzir as chamadas mantendo a lógica de Specification
// mas consolidando em uma abordagem mais eficiente
// se buildSpecification for nulo (ou seja, sem filtros extras).
long total = agentRepository.count(baseSpec);
br.gov.sigefp.rh.api.dto.AgentStatsDTO stats = br.gov.sigefp.rh.api.dto.AgentStatsDTO.builder()
.total(total)
.build();
if (total > 0) {
// Executar consultas de contagem por status usando a mesma especificação
stats.setActive(agentRepository
.count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("ACTIVE"))));
stats.setInactive(agentRepository
.count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("INACTIVE"))));
stats.setSuspended(agentRepository
.count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("SUSPENDED"))));
stats.setTerminated(agentRepository
.count(baseSpec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus("TERMINATED"))));
}
return stats;
}
private Specification<Agent> buildSpecification(
String query,
String status,
UUID ministryId,
UUID orgUnitId,
UUID positionId,
String functionalSituation,
String appointmentType) {
Specification<Agent> spec = Specification.where(null);
if (query != null && !query.isBlank()) {
spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.searchByQuery(query));
}
if (status != null && !status.isBlank()) {
spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasStatus(status));
}
// Se um ministério for selecionado, filtramos por todas as unidades
// subordinadas a ele
if (ministryId != null) {
List<UUID> ministryOrgUnits = orgUnitRepository.findByMinistryId(ministryId)
.stream().map(br.gov.sigefp.org.domain.OrgUnit::getId).collect(Collectors.toList());
// Garantir que a unidade do próprio ministério esteja na lista
if (!ministryOrgUnits.contains(ministryId)) {
ministryOrgUnits.add(ministryId);
}
if (!ministryOrgUnits.isEmpty()) {
spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasOrgUnitIn(ministryOrgUnits));
} else {
spec = spec.and((root, q, cb) -> cb.disjunction());
}
}
if (orgUnitId != null) {
spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasOrgUnit(orgUnitId));
}
if (positionId != null) {
spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasPosition(positionId));
}
if (functionalSituation != null && !functionalSituation.isBlank()) {
spec = spec.and(
br.gov.sigefp.rh.specification.AgentSpecifications.hasFunctionalSituation(functionalSituation));
}
if (appointmentType != null && !appointmentType.isBlank()) {
spec = spec.and(br.gov.sigefp.rh.specification.AgentSpecifications.hasAppointmentType(appointmentType));
}
return spec;
}
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());
}
}
}
public List<CareerTimelineDTO> getTimeline(UUID agentId) {
// Ordenar por data de eficácia ascendente (mais antigo primeiro) para timeline
// cronológica
return careerEventRepository.findByAgentIdOrderByEffectiveDateAsc(agentId).stream()
.map(e -> CareerTimelineDTO.builder().date(e.getEffectiveDate()).eventType(e.getEventType().name())
.eventTypeName(translateEventType(e.getEventType()))
.reason(e.getReason())
.documentRef(e.getDocumentRef())
.totalBaseAmount(e.getTotalBaseAmount())
.cargoAmount(e.getCargoAmount())
.exercicioAmount(e.getExercicioAmount())
.changeSummary(buildChangeSummary(e))
.build())
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<AgentStatusHistoryDTO> getStatusHistory(UUID agentId) {
return statusHistoryRepository.findByAgentIdOrderByChangedAtDesc(agentId).stream()
.map(h -> AgentStatusHistoryDTO.builder()
.id(h.getId())
.previousStatus(h.getPreviousStatus())
.newStatus(h.getNewStatus())
.previousFunctionalSituation(h.getPreviousFunctionalSituation())
.newFunctionalSituation(h.getNewFunctionalSituation())
.eventType(h.getEventType())
.reason(h.getReason())
.changeLog(h.getChangeLog())
.changedAt(h.getChangedAt())
.changedBy(h.getChangedBy())
.build())
.collect(Collectors.toList());
}
private String translateEventType(CareerEventType type) {
return switch (type) {
case ADMISSAO -> "Admissão";
case NOMEACAO_PROVISORIA -> "Nomeação Provisória";
case NOMEACAO_DEFINITIVA -> "Nomeação Definitiva";
case PROMOCAO -> "Promoção";
case PROGRESSAO -> "Progressão";
case SUBSTITUICAO -> "Substituição";
case TRANSFERENCIA -> "Transferência";
case RECLASSIFICACAO -> "Reclassificação";
case COMISSAO_SERVICO -> "Comissão de Serviço";
case TERMINATION -> "Cessação de Funções";
default -> type.name();
};
}
private String buildChangeSummary(CareerEvent e) {
StringBuilder sb = new StringBuilder();
// Verificar mudanças na estrutura salarial
if (e.getNewCategory() != null && !e.getNewCategory().equals(e.getPreviousCategory())) {
sb.append("Mudança de Categoria. ");
}
if (e.getNewGrade() != null && !e.getNewGrade().equals(e.getPreviousGrade())) {
sb.append("Mudança de Grau/Escalão. ");
}
if (e.getNewStep() != null && !e.getNewStep().equals(e.getPreviousStep())) {
sb.append("Mudança de Nível/Step. ");
}
// Verificar mudanças organizacionais
if (e.getNewOrgUnit() != null && !e.getNewOrgUnit().equals(e.getPreviousOrgUnit())) {
sb.append("Transferência de Unidade Orgânica. ");
}
if (e.getNewPosition() != null && !e.getNewPosition().equals(e.getPreviousPosition())) {
sb.append("Mudança de Cargo/Posição. ");
}
// Se não houver mudanças específicas, retornar mensagem genérica baseada no
// tipo de evento
if (sb.length() == 0) {
sb.append("Evento registrado conforme documentação legal.");
}
return sb.toString().trim();
}
private AgentDTO toDTO(Agent agent) {
// Otimização: Os contratos e contas bancárias já devem estar no cache de
// primeiro nível
// devido à busca em lote feita no findAllWithFilters.
// Buscar contrato ativo de forma eficiente
AgentContractDTO activeContractDTO = agent.getContracts().stream()
.filter(c -> Boolean.TRUE.equals(c.getIsActive()))
.findFirst()
.map(this::contractToDTO)
.orElse(null);
// Buscar conta bancária principal de forma eficiente
AgentBankAccountDTO primaryBankAccountDTO = agent.getBankAccounts().stream()
.filter(acc -> Boolean.TRUE.equals(acc.getIsPrimary()))
.findFirst()
.map(this::bankAccountToDTO)
.orElse(null);
// Preencher orgUnit e position do contrato ativo se existir, caso contrário
// usar do agente
UUID orgUnit = activeContractDTO != null && activeContractDTO.getOrgUnit() != null
? activeContractDTO.getOrgUnit()
: agent.getOrgUnit();
UUID position = activeContractDTO != null && activeContractDTO.getPosition() != null
? activeContractDTO.getPosition()
: agent.getPosition();
// Preencher salaryCategory, salaryGrade e salaryStep do contrato ativo se
// existir
UUID salaryCategory = activeContractDTO != null && activeContractDTO.getSalaryCategory() != null
? activeContractDTO.getSalaryCategory()
: agent.getSalaryCategory();
UUID salaryGrade = activeContractDTO != null && activeContractDTO.getSalaryGrade() != null
? activeContractDTO.getSalaryGrade()
: agent.getSalaryGrade();
UUID salaryStep = activeContractDTO != null && activeContractDTO.getSalaryStep() != null
? activeContractDTO.getSalaryStep()
: agent.getSalaryStep();
return AgentDTO.builder()
.id(agent.getId())
.matricula(agent.getMatricula())
.nif(agent.getNif())
.biNumber(agent.getBiNumber())
.fullName(agent.getFullName())
.birthDate(agent.getBirthDate())
.hireDate(agent.getHireDate())
.posseDate(agent.getPosseDate())
.terminationDate(agent.getTerminationDate())
.appointmentType(agent.getAppointmentType())
.functionalSituation(agent.getFunctionalSituation())
.status(agent.getStatus())
.eligibleDependentsCount(agent.getEligibleDependentsCount())
.literaryQualification(agent.getLiteraryQualification())
.salaryCategory(salaryCategory)
.salaryGrade(salaryGrade)
.salaryStep(salaryStep)
.orgUnit(orgUnit)
.position(position)
.nationality(agent.getNationality())
.phone(agent.getPhone())
.email(agent.getEmail())
.address(agent.getAddress())
.activeContract(activeContractDTO)
.primaryBankAccount(primaryBankAccountDTO)
.build();
}
private AgentContractDTO contractToDTO(AgentContract contract) {
return AgentContractDTO.builder()
.id(contract.getId())
.contractType(contract.getContractType())
.startDate(contract.getStartDate())
.endDate(contract.getEndDate())
.weeklyHours(contract.getWeeklyHours())
.baseSalaryRef(contract.getBaseSalaryRef())
.orgUnit(contract.getOrgUnit())
.position(contract.getPosition())
.salaryCategory(contract.getSalaryCategory())
.salaryGrade(contract.getSalaryGrade())
.salaryStep(contract.getSalaryStep())
.legalActReference(contract.getLegalActReference())
.isActive(contract.getIsActive())
.build();
}
private AgentBankAccountDTO bankAccountToDTO(AgentBankAccount account) {
return AgentBankAccountDTO.builder()
.id(account.getId())
.bank(account.getBank())
.branchCode(account.getBranchCode())
.accountNumber(account.getAccountNumber())
.isPrimary(account.getIsPrimary())
.build();
}
private void validateLiteraryQualification(UUID salaryCategory, String literaryQualification) {
if (salaryCategory != null && (literaryQualification == null || literaryQualification.trim().isEmpty())) {
throw new IllegalArgumentException(
"Habilitação Literária é obrigatória quando há Categoria Salarial definida.");
}
}
}
@@ -0,0 +1,149 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.AttendanceRecordRepository;
import br.gov.sigefp.rh.repository.MonthlyAttendanceSheetRepository;
import br.gov.sigefp.rh.repository.AgentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDate;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class AttendanceService {
private final MonthlyAttendanceSheetRepository sheetRepository;
private final AttendanceRecordRepository recordRepository;
private final AgentRepository agentRepository;
public MonthlyAttendanceSheet getOrCreateSheet(UUID orgUnitId, int month, int year) {
return sheetRepository.findByOrgUnitIdAndMonthAndYear(orgUnitId, month, year)
.orElseGet(() -> {
MonthlyAttendanceSheet sheet = MonthlyAttendanceSheet.builder()
.orgUnitId(orgUnitId)
.month(month)
.year(year)
.status(AttendanceSheetStatus.DRAFT)
.build();
return sheetRepository.save(sheet);
});
}
public void approveSheet(UUID sheetId, String approverName) {
MonthlyAttendanceSheet sheet = sheetRepository.findById(sheetId)
.orElseThrow(() -> new BusinessException("Folha não encontrada", "SHEET_NOT_FOUND",
org.springframework.http.HttpStatus.NOT_FOUND));
sheet.setStatus(AttendanceSheetStatus.APPROVED);
sheet.setApprovedBy(approverName);
sheetRepository.save(sheet);
log.info("Folha de Ponto {} aprovada por {}", sheetId, approverName);
}
public void reopenSheet(UUID sheetId) {
MonthlyAttendanceSheet sheet = sheetRepository.findById(sheetId)
.orElseThrow(() -> new BusinessException("Folha não encontrada", "SHEET_NOT_FOUND",
org.springframework.http.HttpStatus.NOT_FOUND));
if (AttendanceSheetStatus.CLOSED.equals(sheet.getStatus())) {
throw new BusinessException("Folha já processada pela contabilidade não pode ser reaberta.", "SHEET_CLOSED",
org.springframework.http.HttpStatus.FORBIDDEN);
}
sheet.setStatus(AttendanceSheetStatus.DRAFT);
sheetRepository.save(sheet);
}
public void importExcel(UUID sheetId, MultipartFile file) {
MonthlyAttendanceSheet sheet = sheetRepository.findById(sheetId)
.orElseThrow(() -> new BusinessException("Folha não encontrada", "SHEET_NOT_FOUND",
org.springframework.http.HttpStatus.NOT_FOUND));
if (!AttendanceSheetStatus.DRAFT.equals(sheet.getStatus())) {
throw new BusinessException("A folha deve estar em Rascunho para importação.", "INVALID_STATUS",
org.springframework.http.HttpStatus.BAD_REQUEST);
}
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
Sheet excelSheet = workbook.getSheetAt(0);
// Clear existing records to avoid duplication or stale data
recordRepository.deleteBySheetId(sheetId);
for (Row row : excelSheet) {
if (row.getRowNum() < 1)
continue; // Skip Header
Cell matriculaCell = row.getCell(0);
if (matriculaCell == null)
continue;
String matricula = getCellValueAsString(matriculaCell);
Agent agent = agentRepository.findByMatricula(matricula)
.orElse(null);
if (agent == null)
continue; // Skip unknown agents
// Iterate Days (Cols 1 to 31)
for (int day = 1; day <= 31; day++) {
Cell dayCell = row.getCell(day);
String code = getCellValueAsString(dayCell);
if (code != null && !code.isEmpty()) {
AttendanceType type = parseType(code);
if (type != null) {
try {
LocalDate date = LocalDate.of(sheet.getYear(), sheet.getMonth(), day);
AttendanceRecord record = AttendanceRecord.builder()
.sheet(sheet)
.agent(agent)
.date(date)
.type(type)
.observation("Importado via Excel")
.build();
recordRepository.save(record);
} catch (Exception e) {
// Invalid date (e.g., Feb 30), ignore
}
}
}
}
}
} catch (IOException e) {
throw new BusinessException("Erro ao ler arquivo Excel", "IO_ERROR",
org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private String getCellValueAsString(Cell cell) {
if (cell == null)
return null;
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue();
case NUMERIC -> String.valueOf((int) cell.getNumericCellValue());
default -> null;
};
}
private AttendanceType parseType(String code) {
return switch (code.toUpperCase().trim()) {
case "F" -> AttendanceType.ABSENCE_UNJUSTIFIED;
case "J" -> AttendanceType.ABSENCE_JUSTIFIED;
case "M" -> AttendanceType.SICK_LEAVE;
case "V" -> AttendanceType.VACATION;
default -> null;
};
}
}
@@ -0,0 +1,78 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.CareerEventRepository;
import br.gov.sigefp.rh.repository.SalaryGridRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.UUID;
/**
* Serviço centralizado para registro de eventos estruturados na vida laboral
* (carreira).
*/
@Service
@RequiredArgsConstructor
@Transactional
public class CareerEventService {
private final CareerEventRepository careerEventRepository;
private final SalaryGridRepository salaryGridRepository;
/**
* Registra um evento de carreira com snapshots e cálculos financeiros.
*/
public void recordEvent(Agent agent, CareerEventType type, String reason, String docRef,
LocalDate effectiveDate, LocalDate publicationDate,
UUID newCategory, UUID newGrade, UUID newStep, UUID newOrg, UUID newPos,
UUID prevCategory, UUID prevGrade, UUID prevStep, UUID prevOrg, UUID prevPos) {
CareerEvent event = CareerEvent.builder()
.agent(agent)
.eventType(type)
.effectiveDate(effectiveDate != null ? effectiveDate : LocalDate.now())
.publicationDate(publicationDate)
.documentRef(docRef)
.reason(reason)
// Snapshots atuais
.previousCategory(prevCategory)
.newCategory(newCategory != null ? newCategory : agent.getSalaryCategory())
.previousGrade(prevGrade)
.newGrade(newGrade != null ? newGrade : agent.getSalaryGrade())
.previousStep(prevStep)
.newStep(newStep != null ? newStep : agent.getSalaryStep())
// Snapshots organizacionais
.previousOrgUnit(prevOrg)
.newOrgUnit(newOrg != null ? newOrg : agent.getOrgUnit())
.previousPosition(prevPos)
.newPosition(newPos != null ? newPos : agent.getPosition())
.createdByUser("system")
.build();
// Cálculo financeiro retroativo ou atual
UUID finalStepId = event.getNewStep();
if (finalStepId != null) {
SalaryGrid grid = salaryGridRepository.findByStepIdAndDate(finalStepId, event.getEffectiveDate());
if (grid != null) {
event.calculateFinancialSplit(grid.getBaseAmount());
}
}
careerEventRepository.save(event);
}
/**
* Versão simplificada para eventos que não alteram carreira (ex: apenas mudança
* de referência legal ou situação funcional).
*/
public void recordSimpleEvent(Agent agent, CareerEventType type, String reason, String docRef,
LocalDate effectiveDate) {
recordEvent(agent, type, reason, docRef, effectiveDate, null,
null, null, null, null, null,
agent.getSalaryCategory(), agent.getSalaryGrade(), agent.getSalaryStep(),
agent.getOrgUnit(), agent.getPosition());
}
}
@@ -0,0 +1,59 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.api.dto.CareerRegimeDTO;
import br.gov.sigefp.rh.domain.CareerRegime;
import br.gov.sigefp.rh.repository.CareerRegimeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CareerRegimeService {
private final CareerRegimeRepository repository;
public List<CareerRegimeDTO> findAll() {
return repository.findAll().stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Transactional
public CareerRegimeDTO create(CareerRegimeDTO dto) {
CareerRegime entity = CareerRegime.builder()
.code(dto.getCode())
.name(dto.getName())
.description(dto.getDescription())
.build();
return toDTO(repository.save(entity));
}
@Transactional
public CareerRegimeDTO update(java.util.UUID id, CareerRegimeDTO dto) {
CareerRegime entity = repository.findById(id)
.orElseThrow(() -> new RuntimeException("Regime não encontrado"));
entity.setCode(dto.getCode());
entity.setName(dto.getName());
entity.setDescription(dto.getDescription());
return toDTO(repository.save(entity));
}
@Transactional
public void delete(java.util.UUID id) {
repository.deleteById(id);
}
private CareerRegimeDTO toDTO(CareerRegime entity) {
return CareerRegimeDTO.builder()
.id(entity.getId())
.code(entity.getCode())
.name(entity.getName())
.description(entity.getDescription())
.build();
}
}
@@ -0,0 +1,104 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.common.exception.BusinessException;
import br.gov.sigefp.rh.api.dto.PerformanceEvaluationDTO;
import br.gov.sigefp.rh.domain.Agent;
import br.gov.sigefp.rh.domain.CareerEventType;
import br.gov.sigefp.rh.domain.PerformanceEvaluation;
import br.gov.sigefp.rh.repository.PerformanceEvaluationRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Serviço para gestão de avaliações de desempenho e automação de carreira.
* Implementa regras do Decreto nº 12-A/94.
*/
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class PerformanceEvaluationService {
private final PerformanceEvaluationRepository evaluationRepository;
private final CareerEventService careerEventService;
/**
* Finaliza uma avaliação e verifica elegibilidade para promoção.
*/
public void finalizeEvaluation(UUID evaluationId) {
PerformanceEvaluation evaluation = evaluationRepository.findById(evaluationId)
.orElseThrow(() -> new BusinessException("Avaliação não encontrada", "NOT_FOUND",
org.springframework.http.HttpStatus.NOT_FOUND));
if (!"DRAFT".equals(evaluation.getStatus())) {
throw new BusinessException("Apenas avaliações em DRAFT podem ser finalizadas", "INVALID_STATUS",
org.springframework.http.HttpStatus.BAD_REQUEST);
}
evaluation.setStatus("FINAL");
evaluation.updateMentionFromScore();
evaluationRepository.save(evaluation);
// Verifica requisitos para promoção (3 anos de BOM ou MUITO_BOM)
checkPromotionEligibility(evaluation.getAgent());
}
/**
* Busca todas as avaliações com paginação.
*/
@Transactional(readOnly = true)
public Page<PerformanceEvaluationDTO> findAll(Pageable pageable) {
return evaluationRepository.findAll(pageable)
.map(this::toDTO);
}
/**
* Converte entidade para DTO.
*/
private PerformanceEvaluationDTO toDTO(PerformanceEvaluation evaluation) {
Agent agent = evaluation.getAgent();
return PerformanceEvaluationDTO.builder()
.id(evaluation.getId())
.agentId(agent != null ? agent.getId() : null)
.agentName(agent != null ? agent.getFullName() : null)
.agentMatricula(agent != null ? agent.getMatricula() : null)
.referenceYear(evaluation.getReferenceYear())
.score(evaluation.getScore())
.status(evaluation.getStatus())
.mention(evaluation.getMention())
.observations(evaluation.getObservations())
.evaluationDate(evaluation.getEvaluationDate())
.build();
}
/**
* Verifica se o agente atingiu os requisitos para promoção automática.
*/
private void checkPromotionEligibility(Agent agent) {
// Busca as últimas 3 avaliações
List<PerformanceEvaluation> lastEvaluations = evaluationRepository
.findByAgentIdOrderByReferenceYearDesc(agent.getId());
if (lastEvaluations.size() < 3) {
return; // Requer pelo menos 3 anos
}
boolean eligible = lastEvaluations.stream()
.limit(3)
.allMatch(e -> "BOM".equals(e.getMention()) || "MUITO_BOM".equals(e.getMention()));
if (eligible) {
log.info("Agente {} elegível para promoção automática conforme Decreto 12-A/94", agent.getMatricula());
// Aqui poderíamos disparar um evento ou criar um CareerEvent sugerido
// Por enquanto, apenas registramos no log ou enviamos notificação
}
}
}
@@ -0,0 +1,257 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.api.dto.*;
import br.gov.sigefp.rh.domain.SalaryCategory;
import br.gov.sigefp.rh.domain.SalaryGrade;
import br.gov.sigefp.rh.domain.SalaryGrid;
import br.gov.sigefp.rh.domain.SalaryStep;
import br.gov.sigefp.rh.repository.AgentRepository;
import br.gov.sigefp.rh.repository.SalaryCategoryRepository;
import br.gov.sigefp.rh.repository.SalaryGradeRepository;
import br.gov.sigefp.rh.repository.SalaryGridRepository;
import br.gov.sigefp.rh.repository.SalaryStepRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Transactional
public class SalaryStructureService {
private final SalaryCategoryRepository categoryRepository;
private final SalaryGradeRepository gradeRepository;
private final SalaryStepRepository stepRepository;
private final SalaryGridRepository gridRepository;
private final AgentRepository agentRepository;
private final br.gov.sigefp.rh.repository.CareerRegimeRepository careerRegimeRepository;
public SalaryStructureFullDTO getFullStructure() {
List<SalaryCategory> categories = categoryRepository.findAll();
LocalDate today = LocalDate.now();
List<SalaryStructureFullDTO.CategoryGroupDTO> categoryDTOs = categories.stream().map(cat -> {
List<SalaryGrade> grades = gradeRepository.findByCategoryId(cat.getId());
List<SalaryStructureFullDTO.GradeRowDTO> gradeDTOs = grades.stream().map(grade -> {
List<SalaryStructureFullDTO.StepDetailDTO> stepDTOs = grade.getSteps().stream()
.sorted((a, b) -> a.getStepNumber() - b.getStepNumber())
.map(step -> {
// Find the grid entry valid for today
SalaryGrid activeGrid = step.getGrids().stream()
.filter(g -> !today.isBefore(g.getValidFrom()) &&
(g.getValidTo() == null || !today.isAfter(g.getValidTo())))
.findFirst()
.orElse(null);
return SalaryStructureFullDTO.StepDetailDTO.builder()
.id(step.getId())
.stepNumber(step.getStepNumber())
.currentValue(activeGrid != null ? activeGrid.getBaseAmount().doubleValue() : null)
.subsidyAmount(activeGrid != null && activeGrid.getSubsidyAmount() != null
? activeGrid.getSubsidyAmount().doubleValue()
: null)
.grossAmount(activeGrid != null && activeGrid.getGrossAmount() != null
? activeGrid.getGrossAmount().doubleValue()
: null)
.build();
}).collect(Collectors.toList());
return SalaryStructureFullDTO.GradeRowDTO.builder()
.id(grade.getId())
.code(grade.getCode())
.name(grade.getName())
.steps(stepDTOs)
.build();
}).collect(Collectors.toList());
return SalaryStructureFullDTO.CategoryGroupDTO.builder()
.id(cat.getId())
.code(cat.getCode())
.name(cat.getName())
.regimeName(cat.getRegime() != null ? cat.getRegime().getName() : null)
.grades(gradeDTOs)
.build();
}).collect(Collectors.toList());
return SalaryStructureFullDTO.builder().categories(categoryDTOs).build();
}
// --- Category methods ---
public List<SalaryCategoryDTO> getAllCategories() {
return categoryRepository.findAll().stream()
.map(this::toCategoryDTO)
.collect(Collectors.toList());
}
public SalaryCategoryDTO createCategory(SalaryCategoryDTO dto) {
if (categoryRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Categoria com código " + dto.getCode() + " já existe");
}
SalaryCategory entity = new SalaryCategory();
entity.setCode(dto.getCode());
entity.setName(dto.getName());
if (dto.getRegimeId() != null) {
br.gov.sigefp.rh.domain.CareerRegime regime = careerRegimeRepository.findById(dto.getRegimeId())
.orElseThrow(() -> new IllegalArgumentException("Regime não encontrado"));
entity.setRegime(regime);
}
return toCategoryDTO(categoryRepository.save(entity));
}
public SalaryCategoryDTO updateCategory(UUID id, SalaryCategoryDTO dto) {
SalaryCategory category = categoryRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Categoria não encontrada"));
if (!category.getCode().equals(dto.getCode()) && categoryRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Categoria com código " + dto.getCode() + " já existe");
}
category.setCode(dto.getCode());
category.setName(dto.getName());
if (dto.getRegimeId() != null) {
br.gov.sigefp.rh.domain.CareerRegime regime = careerRegimeRepository.findById(dto.getRegimeId())
.orElseThrow(() -> new IllegalArgumentException("Regime não encontrado"));
category.setRegime(regime);
} else {
category.setRegime(null);
}
return toCategoryDTO(categoryRepository.save(category));
}
public void deleteCategory(UUID id) {
if (agentRepository.existsBySalaryCategory(id)) {
throw new IllegalArgumentException(
"Não é possível excluir a categoria pois existem agentes vinculados a ela");
}
categoryRepository.deleteById(id);
}
private SalaryCategoryDTO toCategoryDTO(SalaryCategory category) {
return SalaryCategoryDTO.builder()
.id(category.getId())
.code(category.getCode())
.name(category.getName())
.regimeId(category.getRegime() != null ? category.getRegime().getId() : null)
.regimeName(category.getRegime() != null ? category.getRegime().getName() : null)
.build();
}
// --- Grade methods ---
public List<SalaryGradeDTO> getGradesByCategory(UUID categoryId) {
return gradeRepository.findByCategoryId(categoryId).stream()
.map(this::toGradeDTO)
.collect(Collectors.toList());
}
public SalaryGradeDTO createGrade(SalaryGradeDTO dto) {
SalaryCategory category = categoryRepository.findById(dto.getCategoryId())
.orElseThrow(() -> new IllegalArgumentException("Categoria não encontrada"));
SalaryGrade entity = new SalaryGrade();
entity.setCategory(category);
entity.setCode(dto.getCode());
entity.setName(dto.getName());
return toGradeDTO(gradeRepository.save(entity));
}
public SalaryGradeDTO updateGrade(UUID id, SalaryGradeDTO dto) {
SalaryGrade grade = gradeRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Grau não encontrado"));
grade.setCode(dto.getCode());
grade.setName(dto.getName());
return toGradeDTO(gradeRepository.save(grade));
}
public void deleteGrade(UUID id) {
if (agentRepository.existsBySalaryGrade(id)) {
throw new IllegalArgumentException("Não é possível excluir o grau pois existem agentes vinculados a ele");
}
gradeRepository.deleteById(id);
}
private SalaryGradeDTO toGradeDTO(SalaryGrade grade) {
return SalaryGradeDTO.builder()
.id(grade.getId())
.categoryId(grade.getCategory().getId())
.code(grade.getCode())
.name(grade.getName())
.build();
}
// --- Step methods ---
public List<SalaryStepDTO> getStepsByGrade(UUID gradeId) {
return stepRepository.findByGradeIdOrderByStepNumber(gradeId).stream()
.map(this::toStepDTO)
.collect(Collectors.toList());
}
public SalaryStepDTO createStep(SalaryStepDTO dto) {
SalaryGrade grade = gradeRepository.findById(dto.getGradeId())
.orElseThrow(() -> new IllegalArgumentException("Grade não encontrada"));
SalaryStep entity = new SalaryStep();
entity.setGrade(grade);
entity.setStepNumber(dto.getStepNumber());
return toStepDTO(stepRepository.save(entity));
}
public void deleteStep(UUID id) {
if (agentRepository.existsBySalaryStep(id)) {
throw new IllegalArgumentException(
"Não é possível excluir o escalão pois existem agentes vinculados a ele");
}
stepRepository.deleteById(id);
}
private SalaryStepDTO toStepDTO(SalaryStep step) {
return SalaryStepDTO.builder()
.id(step.getId())
.gradeId(step.getGrade().getId())
.stepNumber(step.getStepNumber())
.build();
}
// --- Grid methods ---
public List<SalaryGridDTO> getGridByStep(UUID stepId) {
return gridRepository.findByStepId(stepId).stream()
.map(this::toGridDTO)
.collect(Collectors.toList());
}
public SalaryGridDTO createGridEntry(SalaryGridDTO dto) {
SalaryStep step = stepRepository.findById(dto.getStepId())
.orElseThrow(() -> new IllegalArgumentException("Step não encontrado"));
SalaryGrid entity = new SalaryGrid();
entity.setStep(step);
entity.setValidFrom(dto.getValidFrom());
entity.setValidTo(dto.getValidTo());
entity.setBaseAmount(dto.getBaseAmount());
return toGridDTO(gridRepository.save(entity));
}
private SalaryGridDTO toGridDTO(SalaryGrid grid) {
return SalaryGridDTO.builder()
.id(grid.getId())
.stepId(grid.getStep().getId())
.validFrom(grid.getValidFrom())
.validTo(grid.getValidTo())
.baseAmount(grid.getBaseAmount())
.subsidyAmount(grid.getSubsidyAmount())
.grossAmount(grid.getGrossAmount())
.build();
}
}
@@ -0,0 +1,117 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.rh.domain.GlobalDeductionRule;
import br.gov.sigefp.rh.domain.TaxBracket;
import br.gov.sigefp.rh.repository.GlobalDeductionRuleRepository;
import br.gov.sigefp.rh.repository.TaxBracketRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import br.gov.sigefp.rh.repository.DeductionTypeRepository;
import java.util.List;
import java.util.UUID;
import java.time.LocalDate;
/**
* Serviço para gestão de regras tributárias e escalões.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class TaxService {
private final GlobalDeductionRuleRepository globalDeductionRuleRepository;
private final TaxBracketRepository taxBracketRepository;
private final DeductionTypeRepository deductionTypeRepository;
// --- Deduction Types ---
@org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_CONFIG')")
public br.gov.sigefp.rh.domain.DeductionType saveDeductionType(br.gov.sigefp.rh.domain.DeductionType type) {
if (type.getId() == null) {
// New: Ensure Code Unique (Repository should handle or we check)
}
return deductionTypeRepository.save(type);
}
public void toggleDeductionTypeStatus(UUID id) {
br.gov.sigefp.rh.domain.DeductionType type = deductionTypeRepository.findById(id)
.orElseThrow(() -> new br.gov.sigefp.common.exception.ResourceNotFoundException("Tipo não encontrado"));
type.setActive(!Boolean.TRUE.equals(type.getActive()));
deductionTypeRepository.save(type);
}
public List<GlobalDeductionRule> findAllRules() {
return globalDeductionRuleRepository.findAll();
}
@org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_CONFIG')")
public GlobalDeductionRule saveRule(GlobalDeductionRule rule) {
// P13: Validação de Sobreposição de Regras Globais
if (rule.getDeductionType() == null) {
throw new IllegalArgumentException("O tipo de desconto é obrigatório.");
}
// Data fim padrão se nula (opcional, mas ajuda na consulta)
LocalDate start = rule.getValidFrom();
LocalDate end = rule.getValidTo() != null ? rule.getValidTo() : LocalDate.of(2099, 12, 31);
List<GlobalDeductionRule> overlaps = globalDeductionRuleRepository.findOverlappingRules(
rule.getDeductionType().getId(), start, end);
for (GlobalDeductionRule existing : overlaps) {
if (!existing.getId().equals(rule.getId())) {
throw new br.gov.sigefp.common.exception.BusinessException(
"Sobreposição de datas detectada com a regra ID: " + existing.getId(),
"RULE_OVERLAP", org.springframework.http.HttpStatus.BAD_REQUEST);
}
}
return globalDeductionRuleRepository.save(rule);
}
public void deleteRule(UUID id) {
globalDeductionRuleRepository.deleteById(id);
}
public List<TaxBracket> findAllBrackets() {
return taxBracketRepository.findAll();
}
@org.springframework.security.access.prepost.PreAuthorize("hasRole('HR_CONFIG')")
public TaxBracket saveBracket(TaxBracket bracket) {
// P14: Validação de Sobreposição de Escalões
if (bracket.getDeductionType() == null) {
throw new IllegalArgumentException("O tipo de desconto é obrigatório.");
}
LocalDate start = bracket.getValidFrom();
LocalDate end = bracket.getValidTo() != null ? bracket.getValidTo() : LocalDate.of(2099, 12, 31);
java.math.BigDecimal lower = bracket.getLowerLimit();
// Se upper for null, tratar como infinito para validação
java.math.BigDecimal upper = bracket.getUpperLimit() != null ? bracket.getUpperLimit()
: new java.math.BigDecimal("999999999");
List<TaxBracket> overlaps = taxBracketRepository.findOverlappingBrackets(
bracket.getDeductionType().getId(), start, end, lower, upper);
for (TaxBracket existing : overlaps) {
if (!existing.getId().equals(bracket.getId())) {
throw new br.gov.sigefp.common.exception.BusinessException(
"Sobreposição de escalões detectada com o ID: " + existing.getId(),
"BRACKET_OVERLAP", org.springframework.http.HttpStatus.BAD_REQUEST);
}
}
return taxBracketRepository.save(bracket);
}
public void deleteBracket(UUID id) {
taxBracketRepository.deleteById(id);
}
public List<br.gov.sigefp.rh.domain.DeductionType> findAllDeductionTypes() {
return deductionTypeRepository.findAll();
}
}
@@ -0,0 +1,62 @@
package br.gov.sigefp.rh.specification;
import br.gov.sigefp.rh.domain.Agent;
import org.springframework.data.jpa.domain.Specification;
import java.util.UUID;
/**
* Specifications para filtrar agentes de forma dinâmica.
*/
public class AgentSpecifications {
public static Specification<Agent> hasStatus(String status) {
return (root, query, cb) -> status == null ? null : cb.equal(root.get("status"), status);
}
public static Specification<Agent> hasOrgUnit(UUID orgUnitId) {
return (root, query, cb) -> {
if (orgUnitId == null)
return null;
System.out.println("DEBUG SPEC: Filtering by orgUnit ID: " + orgUnitId);
return cb.equal(root.get("orgUnit"), orgUnitId);
};
}
public static Specification<Agent> hasOrgUnitIn(java.util.List<UUID> orgUnitIds) {
return (root, query, cb) -> {
if (orgUnitIds == null || orgUnitIds.isEmpty())
return null;
System.out.println("DEBUG SPEC: Filtering by orgUnit LIST: " + orgUnitIds.size() + " units");
return root.get("orgUnit").in(orgUnitIds);
};
}
public static Specification<Agent> hasPosition(UUID positionId) {
return (root, query, cb) -> positionId == null ? null : cb.equal(root.get("position"), positionId);
}
public static Specification<Agent> hasFunctionalSituation(String functionalSituation) {
return (root, query, cb) -> functionalSituation == null ? null
: cb.equal(root.get("functionalSituation"), functionalSituation);
}
public static Specification<Agent> hasAppointmentType(String appointmentType) {
return (root, query, cb) -> appointmentType == null ? null
: cb.equal(root.get("appointmentType"), appointmentType);
}
public static Specification<Agent> searchByQuery(String query) {
return (root, criteriaQuery, cb) -> {
if (query == null || query.trim().isEmpty()) {
return null;
}
String searchPattern = "%" + query.toLowerCase() + "%";
return cb.or(
cb.like(cb.lower(root.get("fullName")), searchPattern),
cb.like(cb.lower(root.get("matricula")), searchPattern),
cb.like(cb.lower(root.get("nif")), searchPattern),
cb.like(cb.lower(root.get("biNumber")), searchPattern));
};
}
}
@@ -0,0 +1,228 @@
package br.gov.sigefp.rh.service;
import br.gov.sigefp.budget.integration.BudgetIntegrationService;
import br.gov.sigefp.rh.domain.*;
import br.gov.sigefp.rh.repository.*; // Assume this covers FamilyAllowanceTableRepository if it's in the same package
import br.gov.sigefp.common.exception.BusinessException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class PayrollServiceTest {
@Mock
private PayrollPeriodRepository payrollPeriodRepository;
@Mock
private PayrollRunRepository payrollRunRepository;
@Mock
private PayrollItemRepository payrollItemRepository;
@Mock
private AgentRepository agentRepository;
@Mock
private SalaryGridRepository salaryGridRepository;
@Mock
private EarningTypeRepository earningTypeRepository;
@Mock
private GlobalDeductionRuleRepository globalDeductionRuleRepository;
@Mock
private TaxBracketRepository taxBracketRepository;
@Mock
private AttendanceRecordRepository attendanceRecordRepository;
@Mock
private AbsenceRepository absenceRepository;
@Mock
private br.gov.sigefp.budget.repository.BudgetLineRepository budgetLineRepository;
@Mock
private br.gov.sigefp.budget.repository.FiscalYearRepository fiscalYearRepository;
@Mock
private BudgetIntegrationService budgetIntegrationService;
@Mock
private AgentContractRepository agentContractRepository;
@Mock
private FamilyAllowanceTableRepository familyAllowanceTableRepository;
@Mock
private br.gov.sigefp.common.service.PaymentGenerator paymentGenerator;
@Mock
private DeductionTypeRepository deductionTypeRepository;
@InjectMocks
private PayrollService payrollService;
private UUID payrollRunId;
private PayrollRun payrollRun;
private Agent agent;
private EarningType baseSalaryType;
private EarningType familyAllowanceType;
private EarningType subsidyType;
private AgentContract contract;
@BeforeEach
void setUp() {
payrollRunId = UUID.randomUUID();
PayrollPeriod period = PayrollPeriod.builder()
.fiscalYear(2024)
.month(1)
.startDate(LocalDate.of(2024, 1, 1))
.endDate(LocalDate.of(2024, 1, 31))
.build();
payrollRun = PayrollRun.builder()
.id(payrollRunId)
.period(period)
.status("PENDING")
.build();
agent = Agent.builder()
.id(UUID.randomUUID())
.fullName("Test Agent")
.status("ACTIVE")
.salaryStep(UUID.randomUUID())
.posseDate(LocalDate.of(2020, 1, 1))
.eligibleDependentsCount(2)
.build();
contract = AgentContract.builder()
.agent(agent)
.isActive(true)
.startDate(LocalDate.of(2020, 1, 1))
.build();
baseSalaryType = EarningType.builder()
.id(UUID.randomUUID())
.code("SALARIO_BASE")
.name("Vencimento Base")
.build();
familyAllowanceType = EarningType.builder()
.id(UUID.randomUUID())
.code("ABONO_FAMILIA")
.name("Abono de Família")
.build();
subsidyType = EarningType.builder()
.id(UUID.randomUUID())
.code("SUBSIDIO")
.name("Subsídio")
.build();
}
@Test
@DisplayName("Deve gerar itens de folha com cálculos de INPS e IRPS corretos")
void generatePayrollItems_WithSuccess() {
// Mocking
when(payrollRunRepository.findById(payrollRunId)).thenReturn(Optional.of(payrollRun));
when(earningTypeRepository.findByCode("SALARIO_BASE")).thenReturn(Optional.of(baseSalaryType));
when(earningTypeRepository.findByCode("ABONO_FAMILIA")).thenReturn(Optional.of(familyAllowanceType));
when(earningTypeRepository.findByCode("SUBSIDIO")).thenReturn(Optional.of(subsidyType));
when(agentRepository.findByStatus("ACTIVE")).thenReturn(List.of(agent));
// MOCK BATCH FETCHES
when(agentContractRepository.findByAgentIdIn(anyList())).thenReturn(List.of(contract));
when(attendanceRecordRepository.findByAgentIdInAndDateRange(any(), any(), any())).thenReturn(List.of());
when(absenceRepository.findByAgentIdInAndDateRange(any(), any(), any())).thenReturn(List.of());
// when(budgetLineRepository.findByFiscalYearId(any())).thenReturn(List.of());
// // Not strictly needed if returns empty by default mock? better explicit.
// Salário Base: 100.000
BigDecimal baseAmount = new BigDecimal("100000");
SalaryStep step = SalaryStep.builder().build();
step.setId(agent.getSalaryStep()); // Ensure ID matches agent
SalaryGrid salaryGrid = SalaryGrid.builder()
.baseAmount(baseAmount)
.step(step)
.validFrom(LocalDate.of(2020, 1, 1))
.build();
// MOCK findAll for Batch Fetch
when(salaryGridRepository.findAll()).thenReturn(List.of(salaryGrid));
// when(salaryGridRepository.findByStepIdAndDate(any(),
// any())).thenReturn(salaryGrid); // Removing old mock
// Regras Globais: INPS (7%) e Selo (0.3%)
DeductionType inpsType = DeductionType.builder().code("INPS").name("INPS").build();
GlobalDeductionRule inpsRule = GlobalDeductionRule.builder()
.deductionType(inpsType)
.percentage(new BigDecimal("0.07"))
.build();
// MOCK FISCAL YEAR & BUDGET LINE
br.gov.sigefp.budget.domain.FiscalYear fy = br.gov.sigefp.budget.domain.FiscalYear.builder()
.id(UUID.randomUUID())
.year(2024)
.build();
when(fiscalYearRepository.findByYear(2024)).thenReturn(Optional.of(fy));
when(budgetLineRepository.findByFiscalYearId(any())).thenReturn(List.of());
DeductionType seloType = DeductionType.builder().code("SELO").name("Imposto de Selo").build();
GlobalDeductionRule seloRule = GlobalDeductionRule.builder()
.deductionType(seloType)
.percentage(new BigDecimal("0.003"))
.build();
when(globalDeductionRuleRepository.findActiveRules(any())).thenReturn(List.of(inpsRule, seloRule));
// IRPS: Faixa de 10% para base > 50.000 (Exemplo simples)
DeductionType irpsType = DeductionType.builder().code("IRPS").name("IRPS").build();
TaxBracket irpsBracket = TaxBracket.builder()
.deductionType(irpsType)
.lowerLimit(new BigDecimal("50001"))
.upperLimit(new BigDecimal("150000"))
.ratePercentage(new BigDecimal("0.10"))
.excessDeduction(new BigDecimal("5000"))
.build();
when(taxBracketRepository.findActiveBrackets(any())).thenReturn(List.of(irpsBracket));
// Execute
payrollService.generatePayrollItems(payrollRunId);
// Verify Items Saved
// 1. Salário Base (100.000)
// 2. Abono Família (2000)
// Bruto = 102.000
// 3. INPS (102.000 * 0.07 = 7.140)
// 4. Selo (102.000 * 0.003 = 306)
// Base IRPS = 102.000 - 7.140 = 94.860
// 5. IRPS (94.860 * 0.10 - 5.000 = 9.486 - 5.000 = 4.486)
verify(payrollItemRepository, atLeast(5)).save(any(PayrollItem.class));
// Vamos verificar os valores específicos se possível via capture (opcional aqui
// para brevidade)
verify(payrollRunRepository).save(argThat(run -> "GENERATED".equals(run.getStatus())));
}
@Test
@DisplayName("Deve falhar ao processar folha sem itens orçamentados")
void processPayrollRun_FailsWhenBudgetLineMissing() {
payrollRun.setStatus("GENERATED");
when(payrollRunRepository.findById(payrollRunId)).thenReturn(Optional.of(payrollRun));
PayrollItem itemWithoutBudget = PayrollItem.builder()
.id(UUID.randomUUID())
.totalAmount(new BigDecimal("1000"))
.budgetLine(null) // Erro proposital
.build();
when(payrollItemRepository.findByPayrollRunId(payrollRunId)).thenReturn(List.of(itemWithoutBudget));
assertThrows(BusinessException.class, () -> payrollService.processPayrollRun(payrollRunId));
}
}