feat: otimização de performance e ajustes finais
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?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-budget</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>SIGEFP Budget</name>
|
||||
<description>Módulo de orçamento: exercícios, linhas orçamentais, execução</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>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.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>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package br.gov.sigefp.budget.api;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetEntryDTO;
|
||||
import br.gov.sigefp.budget.api.dto.CreateBudgetEntryDTO;
|
||||
import br.gov.sigefp.budget.service.BudgetEntryService;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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/budget/entries")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BudgetEntryController {
|
||||
|
||||
private final BudgetEntryService budgetEntryService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<BudgetEntryDTO> create(@Valid @RequestBody CreateBudgetEntryDTO dto) {
|
||||
try {
|
||||
BudgetEntryDTO created = budgetEntryService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
log.warn("Recurso não encontrado ao criar entrada orçamentária: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||
} catch (BusinessException e) {
|
||||
log.warn("Erro de negócio ao criar entrada orçamentária: {}", e.getMessage());
|
||||
return ResponseEntity.status(e.getStatus()).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao criar entrada orçamentária", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<BudgetEntryDTO>> findByBudgetLine(
|
||||
@RequestParam(name = "budgetLineId") UUID budgetLineId,
|
||||
@RequestParam(name = "page", defaultValue = "0") int page,
|
||||
@RequestParam(name = "size", defaultValue = "20") int size,
|
||||
@RequestParam(name = "sortBy", defaultValue = "transactionDate") String sortBy,
|
||||
@RequestParam(name = "sortDirection", defaultValue = "DESC") String sortDirection) {
|
||||
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
Page<BudgetEntryDTO> result = budgetEntryService.findByBudgetLineId(budgetLineId, pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package br.gov.sigefp.budget.api;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetExecutionDTO;
|
||||
import br.gov.sigefp.budget.service.BudgetExecutionService;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.InsufficientBudgetException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 execuções orçamentárias.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/budget/execution")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BudgetExecutionController {
|
||||
|
||||
private final BudgetExecutionService budgetExecutionService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(name = "budgetLineId", required = false) UUID budgetLineId,
|
||||
@RequestParam(name = "periodId", required = false) Long periodId,
|
||||
@RequestParam(name = "movementType", required = false) String movementType,
|
||||
@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, "createdAt");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
Page<BudgetExecutionDTO> result = budgetExecutionService.findByFilters(
|
||||
budgetLineId, periodId, movementType, pageable);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<BudgetExecutionDTO> registerExecution(@Valid @RequestBody BudgetExecutionDTO dto) {
|
||||
try {
|
||||
BudgetExecutionDTO created = budgetExecutionService.registerExecution(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
log.warn("Recurso não encontrado ao registrar execução: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||
} catch (InsufficientBudgetException e) {
|
||||
log.warn("Saldo insuficiente ao registrar execução: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).build();
|
||||
} catch (BusinessException e) {
|
||||
log.warn("Erro de negócio ao registrar execução: {}", e.getMessage());
|
||||
return ResponseEntity.status(e.getStatus()).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao registrar execução", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package br.gov.sigefp.budget.api;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetLineDTO;
|
||||
import br.gov.sigefp.budget.service.BudgetLineService;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 linhas orçamentárias.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/budget/lines")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BudgetLineController {
|
||||
|
||||
private final BudgetLineService budgetLineService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<BudgetLineDTO>> findAll(
|
||||
@RequestParam(name = "fiscalYearId", required = false) UUID fiscalYearId,
|
||||
@RequestParam(name = "ministryId", required = false) UUID ministryId,
|
||||
@RequestParam(name = "orgUnitId", required = false) UUID orgUnitId,
|
||||
@RequestParam(name = "search", required = false) String search,
|
||||
@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 = "ASC") String sortDirection) {
|
||||
|
||||
Sort sort = sortBy != null
|
||||
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
|
||||
: Sort.by(Sort.Direction.ASC, "code");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<BudgetLineDTO> result = budgetLineService.findWithFilters(fiscalYearId, ministryId, orgUnitId, search,
|
||||
pageable);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<BudgetLineDTO> findById(@PathVariable UUID id) {
|
||||
try {
|
||||
BudgetLineDTO dto = budgetLineService.findById(id);
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (ResourceNotFoundException | IllegalArgumentException e) {
|
||||
log.warn("Recurso não encontrado ao buscar linha orçamentária: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao buscar linha orçamentária", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<BudgetLineDTO> create(@Valid @RequestBody BudgetLineDTO dto) {
|
||||
try {
|
||||
BudgetLineDTO created = budgetLineService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (BusinessException | IllegalArgumentException e) {
|
||||
log.warn("Erro de negócio ao criar linha orçamentária: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao criar linha orçamentária", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<BudgetLineDTO> update(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody BudgetLineDTO dto) {
|
||||
try {
|
||||
BudgetLineDTO updated = budgetLineService.update(id, dto);
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (ResourceNotFoundException | IllegalArgumentException e) {
|
||||
log.warn("Recurso não encontrado ao atualizar linha orçamentária: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (BusinessException e) {
|
||||
log.warn("Erro de negócio ao atualizar linha orçamentária: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao atualizar linha orçamentária", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package br.gov.sigefp.budget.api;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.EconomicClassificationDTO;
|
||||
import br.gov.sigefp.budget.service.EconomicClassificationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/budget/economic-classifications")
|
||||
@RequiredArgsConstructor
|
||||
public class EconomicClassificationController {
|
||||
|
||||
private final EconomicClassificationService service;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<EconomicClassificationDTO>> findAll(
|
||||
@RequestParam(required = false) String type) {
|
||||
return ResponseEntity.ok(service.findAll(type));
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package br.gov.sigefp.budget.api;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.FiscalYearDTO;
|
||||
import br.gov.sigefp.budget.service.FiscalYearService;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 anos fiscais.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/budget/fiscal-years")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class FiscalYearController {
|
||||
|
||||
private final FiscalYearService fiscalYearService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<FiscalYearDTO>> findAll(
|
||||
@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, "year");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<FiscalYearDTO> result = fiscalYearService.findAll(pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/current")
|
||||
public ResponseEntity<FiscalYearDTO> getCurrent() {
|
||||
try {
|
||||
FiscalYearDTO dto = fiscalYearService.getCurrentFiscalYear();
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (ResourceNotFoundException | IllegalArgumentException e) {
|
||||
log.warn("Ano fiscal corrente não encontrado");
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao buscar ano fiscal corrente", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<FiscalYearDTO> findById(@PathVariable(name = "id") UUID id) {
|
||||
try {
|
||||
FiscalYearDTO dto = fiscalYearService.findById(id);
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (ResourceNotFoundException | IllegalArgumentException e) {
|
||||
log.warn("Ano fiscal não encontrado: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao buscar ano fiscal", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<FiscalYearDTO> create(@Valid @RequestBody FiscalYearDTO dto) {
|
||||
try {
|
||||
FiscalYearDTO created = fiscalYearService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (BusinessException | IllegalArgumentException e) {
|
||||
log.warn("Erro de negócio ao criar ano fiscal: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao criar ano fiscal", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/open")
|
||||
public ResponseEntity<FiscalYearDTO> open(@PathVariable(name = "id") UUID id) {
|
||||
try {
|
||||
FiscalYearDTO updated = fiscalYearService.open(id);
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (ResourceNotFoundException | IllegalArgumentException e) {
|
||||
log.warn("Ano fiscal não encontrado ao abrir: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (BusinessException e) {
|
||||
log.warn("Erro de negócio ao abrir ano fiscal: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao abrir ano fiscal", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/close")
|
||||
public ResponseEntity<FiscalYearDTO> close(@PathVariable(name = "id") UUID id) {
|
||||
try {
|
||||
FiscalYearDTO updated = fiscalYearService.close(id);
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (ResourceNotFoundException | IllegalArgumentException e) {
|
||||
log.warn("Ano fiscal não encontrado ao fechar: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (BusinessException e) {
|
||||
log.warn("Erro de negócio ao fechar ano fiscal: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao fechar ano fiscal", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package br.gov.sigefp.budget.api.dto;
|
||||
|
||||
import br.gov.sigefp.budget.domain.BudgetEntryType;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class BudgetEntryDTO {
|
||||
private UUID id;
|
||||
private UUID budgetLineId;
|
||||
private BudgetEntryType type;
|
||||
private BigDecimal amount;
|
||||
private LocalDate transactionDate;
|
||||
private String documentReference;
|
||||
private String description;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package br.gov.sigefp.budget.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de execução orçamentária.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BudgetExecutionDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotNull(message = "ID da linha orçamentária é obrigatório")
|
||||
private UUID budgetLineId;
|
||||
|
||||
private Long periodId;
|
||||
|
||||
@NotBlank(message = "Tipo de movimento é obrigatório")
|
||||
@Size(max = 50, message = "Tipo de movimento deve ter no máximo 50 caracteres")
|
||||
private String movementType; // COMMITMENT, LIQUIDATION, PAYMENT
|
||||
|
||||
@NotNull(message = "Valor é obrigatório")
|
||||
@Positive(message = "Valor deve ser positivo")
|
||||
private BigDecimal amount;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@NotBlank(message = "Módulo de origem é obrigatório")
|
||||
@Size(max = 50, message = "Módulo de origem deve ter no máximo 50 caracteres")
|
||||
private String sourceModule;
|
||||
|
||||
private UUID referenceId;
|
||||
|
||||
// Campos enriquecidos para exibição
|
||||
private String budgetLineCode;
|
||||
private String description;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package br.gov.sigefp.budget.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.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de linha orçamentária.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BudgetLineDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotNull(message = "ID do ano fiscal é obrigatório")
|
||||
private UUID fiscalYearId;
|
||||
|
||||
@NotBlank(message = "Código é obrigatório")
|
||||
@Size(min = 1, max = 50, message = "Código deve ter entre 1 e 50 caracteres")
|
||||
private String code;
|
||||
|
||||
@NotBlank(message = "Descrição é obrigatória")
|
||||
@Size(max = 500, message = "Descrição deve ter no máximo 500 caracteres")
|
||||
private String description;
|
||||
|
||||
@NotNull(message = "ID do ministério é obrigatório")
|
||||
private UUID ministryId;
|
||||
|
||||
@NotNull(message = "ID da unidade organizacional é obrigatório")
|
||||
private UUID orgUnitId;
|
||||
|
||||
@NotBlank(message = "Classificação econômica é obrigatória")
|
||||
@Size(max = 100, message = "Classificação econômica deve ter no máximo 100 caracteres")
|
||||
private String economicClass;
|
||||
|
||||
// Campos calculados
|
||||
private BigDecimal totalAllocated;
|
||||
private BigDecimal totalCommitted;
|
||||
private BigDecimal availableBalance;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package br.gov.sigefp.budget.api.dto;
|
||||
|
||||
import br.gov.sigefp.budget.domain.BudgetEntryType;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class CreateBudgetEntryDTO {
|
||||
@NotNull(message = "Linha orçamentária é obrigatória")
|
||||
private UUID budgetLineId;
|
||||
|
||||
@NotNull(message = "Tipo de movimento é obrigatório")
|
||||
private BudgetEntryType type;
|
||||
|
||||
@NotNull(message = "Valor é obrigatório")
|
||||
@DecimalMin(value = "0.01", message = "Valor deve ser maior que zero")
|
||||
private BigDecimal amount;
|
||||
|
||||
@NotNull(message = "Data da transação é obrigatória")
|
||||
private LocalDate transactionDate;
|
||||
|
||||
@NotBlank(message = "Referência documental é obrigatória (ex: Lei, Decreto)")
|
||||
private String documentReference;
|
||||
|
||||
private String description;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package br.gov.sigefp.budget.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class EconomicClassificationDTO {
|
||||
private UUID id;
|
||||
private String code;
|
||||
private String description;
|
||||
private String type;
|
||||
private String uemoaCode;
|
||||
|
||||
// Helper for displaying "Code - Description"
|
||||
public String getLabel() {
|
||||
return code + " - " + description;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package br.gov.sigefp.budget.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
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;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de ano fiscal.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FiscalYearDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotNull(message = "Ano é obrigatório")
|
||||
@Min(value = 2000, message = "Ano deve ser maior ou igual a 2000")
|
||||
@Max(value = 2100, message = "Ano deve ser menor ou igual a 2100")
|
||||
private Integer year;
|
||||
|
||||
@NotNull(message = "Data de início é obrigatória")
|
||||
private LocalDate startDate;
|
||||
|
||||
@NotNull(message = "Data de fim é obrigatória")
|
||||
private LocalDate endDate;
|
||||
|
||||
@NotBlank(message = "Status é obrigatório")
|
||||
private String status; // DRAFT, OPEN, CLOSED
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package br.gov.sigefp.budget.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma alocação orçamentária.
|
||||
* Usa BigDecimal para valores financeiros.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "budget_allocation", indexes = {
|
||||
@Index(name = "idx_budget_allocation_line", columnList = "budget_line_id"),
|
||||
@Index(name = "idx_budget_allocation_created", columnList = "created_at")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class BudgetAllocation extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "budget_line_id", nullable = false)
|
||||
private BudgetLine budgetLine;
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2, name = "initial_amount")
|
||||
private BigDecimal initialAmount; // Valor inicial alocado
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2, name = "adjustment_amount")
|
||||
@Builder.Default
|
||||
private BigDecimal adjustmentAmount = BigDecimal.ZERO; // Ajustes (positivos ou negativos)
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package br.gov.sigefp.budget.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "budget_entry", indexes = {
|
||||
@Index(name = "idx_budget_entry_line", columnList = "budget_line_id"),
|
||||
@Index(name = "idx_budget_entry_date", columnList = "transaction_date")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class BudgetEntry extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "budget_line_id", nullable = false)
|
||||
private BudgetLine budgetLine;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 30)
|
||||
private BudgetEntryType type;
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "transaction_date", nullable = false)
|
||||
private LocalDate transactionDate; // Data da Lei/Decreto
|
||||
|
||||
@Column(nullable = false, length = 100, name = "document_reference")
|
||||
private String documentReference; // Ex: "Lei do Orçamento 2024", "Decreto 12/2024"
|
||||
|
||||
@Column(length = 255)
|
||||
private String description;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package br.gov.sigefp.budget.domain;
|
||||
|
||||
public enum BudgetEntryType {
|
||||
INITIAL_ALLOCATION, // Dotação Inicial (Lei do Orçamento)
|
||||
SUPPLEMENTARY_CREDIT, // Crédito Suplementar
|
||||
SPECIAL_CREDIT, // Crédito Especial
|
||||
CANCELLATION, // Anulação de Dotação
|
||||
TRANSFER_IN, // Transferência Recebida
|
||||
TRANSFER_OUT // Transferência Enviada
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package br.gov.sigefp.budget.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma execução orçamentária.
|
||||
* Usa BigDecimal para valores financeiros.
|
||||
* Usa periodId (Long) para referência ao período de folha, desacoplando do módulo RH.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "budget_execution", indexes = {
|
||||
@Index(name = "idx_budget_execution_line", columnList = "budget_line_id"),
|
||||
@Index(name = "idx_budget_execution_period", columnList = "period_id"),
|
||||
@Index(name = "idx_budget_execution_created", columnList = "created_at"),
|
||||
@Index(name = "idx_budget_execution_source", columnList = "source_module,reference_id")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class BudgetExecution extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "budget_line_id", nullable = false)
|
||||
private BudgetLine budgetLine;
|
||||
|
||||
@Column(name = "period_id")
|
||||
private Long periodId; // Referência ao período de folha (rh.payroll_period) - desacoplado
|
||||
|
||||
@Column(nullable = false, length = 50, name = "movement_type")
|
||||
private String movementType; // COMMITMENT, PAYMENT, REVERSAL - preparado para enum
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(nullable = false, length = 50, name = "source_module")
|
||||
private String sourceModule; // rh, treasury, etc. - módulo que originou a execução
|
||||
|
||||
@Column(name = "reference_id")
|
||||
private UUID referenceId; // ID da entidade no módulo de origem (ex: payment_id, payroll_item_id)
|
||||
}
|
||||
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package br.gov.sigefp.budget.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma linha orçamentária.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "budget_line", indexes = {
|
||||
@Index(name = "idx_budget_line_code", columnList = "code"),
|
||||
@Index(name = "idx_budget_line_fiscal_year", columnList = "fiscal_year_id"),
|
||||
@Index(name = "idx_budget_line_ministry", columnList = "ministry_id"),
|
||||
@Index(name = "idx_budget_line_org_unit", columnList = "org_unit_id")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class BudgetLine extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "fiscal_year_id", nullable = false)
|
||||
private FiscalYear fiscalYear;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false, length = 500)
|
||||
private String description;
|
||||
|
||||
@Column(name = "ministry_id", nullable = false)
|
||||
private UUID ministry; // Referência ao ministério (org.ministry)
|
||||
|
||||
@Column(name = "org_unit_id", nullable = false)
|
||||
private UUID orgUnit; // Referência à unidade organizacional (org.org_unit)
|
||||
|
||||
@Column(nullable = false, length = 100, name = "economic_class")
|
||||
private String economicClass; // Classificação econômica
|
||||
|
||||
@org.hibernate.annotations.Formula("(SELECT COALESCE(SUM(be.amount), 0) FROM budget_entry be WHERE be.budget_line_id = id)")
|
||||
private java.math.BigDecimal totalAllocated;
|
||||
|
||||
@org.hibernate.annotations.Formula("(SELECT COALESCE(SUM(bex.amount), 0) FROM budget_execution bex WHERE bex.budget_line_id = id AND bex.movement_type = 'COMMITMENT')")
|
||||
private java.math.BigDecimal totalCommitted;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package br.gov.sigefp.budget.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;
|
||||
|
||||
@Entity
|
||||
@Table(name = "economic_classification")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class EconomicClassification extends BaseEntity {
|
||||
|
||||
@Column(nullable = false, unique = true, length = 20)
|
||||
private String code; // e.g. 6111
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
private String description; // e.g. Salários do pessoal do quadro
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String type; // REVENUE (Receita) or EXPENSE (Despesa)
|
||||
|
||||
@Column(length = 20)
|
||||
private String uemoaCode; // Optional mapping (e.g. 010101 for 7111)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package br.gov.sigefp.budget.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* Entidade que representa um ano fiscal.
|
||||
* Usa LocalDate para datas de início e fim.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "fiscal_year", indexes = {
|
||||
@Index(name = "idx_fiscal_year_year", columnList = "year"),
|
||||
@Index(name = "idx_fiscal_year_status", columnList = "status")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class FiscalYear extends BaseEntity {
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private Integer year;
|
||||
|
||||
@Column(nullable = false, name = "start_date")
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column(nullable = false, name = "end_date")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "OPEN"; // OPEN, CLOSED, PLANNING - preparado para enum
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package br.gov.sigefp.budget.integration;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetExecutionDTO;
|
||||
import br.gov.sigefp.budget.service.BudgetExecutionService;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.InsufficientBudgetException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Serviço de integração para criar execuções orçamentárias a partir de outros
|
||||
* módulos.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BudgetIntegrationService {
|
||||
|
||||
private final BudgetExecutionService budgetExecutionService;
|
||||
|
||||
/**
|
||||
* Cria execução orçamentária do tipo COMMITMENT a partir de um item de folha.
|
||||
*/
|
||||
@Transactional
|
||||
public void createCommitmentFromPayrollItem(UUID budgetLineId, Long periodId, BigDecimal amount, UUID referenceId) {
|
||||
if (budgetLineId == null || periodId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BudgetExecutionDTO execution = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.periodId(periodId)
|
||||
.movementType("COMMITMENT")
|
||||
.amount(amount)
|
||||
.sourceModule("rh")
|
||||
.referenceId(referenceId)
|
||||
.build();
|
||||
|
||||
budgetExecutionService.registerExecution(execution);
|
||||
} catch (InsufficientBudgetException e) {
|
||||
log.error("Saldo insuficiente ao criar execução orçamentária (COMMITMENT)", e);
|
||||
throw e; // Re-throw exceção específica
|
||||
} catch (BusinessException e) {
|
||||
log.error("Erro de negócio ao criar execução orçamentária (COMMITMENT)", e);
|
||||
throw e; // Re-throw exceção específica
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao criar execução orçamentária (COMMITMENT)", e);
|
||||
throw new BusinessException("Erro ao criar execução orçamentária (COMMITMENT)",
|
||||
"EXECUTION_ERROR", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria execução orçamentária do tipo PAYMENT a partir de um pagamento da
|
||||
* tesouraria.
|
||||
*/
|
||||
@Transactional
|
||||
public void createPaymentFromTreasury(UUID budgetLineId, Long periodId, BigDecimal amount, UUID referenceId) {
|
||||
if (budgetLineId == null || periodId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BudgetExecutionDTO execution = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.periodId(periodId)
|
||||
.movementType("PAYMENT")
|
||||
.amount(amount)
|
||||
.sourceModule("treasury")
|
||||
.referenceId(referenceId)
|
||||
.build();
|
||||
|
||||
budgetExecutionService.registerExecution(execution);
|
||||
} catch (InsufficientBudgetException e) {
|
||||
log.error("Saldo insuficiente ao criar execução orçamentária (PAYMENT)", e);
|
||||
throw e;
|
||||
} catch (BusinessException e) {
|
||||
log.error("Erro de negócio ao criar execução orçamentária (PAYMENT)", e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao criar execução orçamentária (PAYMENT)", e);
|
||||
throw new BusinessException("Erro ao criar execução orçamentária (PAYMENT)",
|
||||
"EXECUTION_ERROR", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria execução orçamentária do tipo LIQUIDATION a partir de um item de folha
|
||||
* (no encerramento).
|
||||
*/
|
||||
@Transactional
|
||||
public void createLiquidationFromPayrollItem(UUID budgetLineId, Long periodId, BigDecimal amount,
|
||||
UUID referenceId) {
|
||||
if (budgetLineId == null || periodId == null || amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BudgetExecutionDTO execution = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.periodId(periodId)
|
||||
.movementType("LIQUIDATION")
|
||||
.amount(amount)
|
||||
.sourceModule("rh")
|
||||
.referenceId(referenceId)
|
||||
.build();
|
||||
|
||||
budgetExecutionService.registerExecution(execution);
|
||||
} catch (InsufficientBudgetException e) {
|
||||
log.error("Saldo insuficiente ao criar execução orçamentária (LIQUIDATION)", e);
|
||||
throw e;
|
||||
} catch (BusinessException e) {
|
||||
log.error("Erro de negócio ao criar execução orçamentária (LIQUIDATION)", e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Erro inesperado ao criar execução orçamentária (LIQUIDATION)", e);
|
||||
throw new BusinessException("Erro ao criar execução orçamentária (LIQUIDATION)",
|
||||
"EXECUTION_ERROR", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converte um PayrollPeriod (fiscalYear + month) para um periodId Long (formato
|
||||
* YYYYMM).
|
||||
*/
|
||||
public static Long toPeriodId(Integer fiscalYear, Integer month) {
|
||||
if (fiscalYear == null || month == null) {
|
||||
return null;
|
||||
}
|
||||
return Long.valueOf(fiscalYear * 100 + month);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package br.gov.sigefp.budget.repository;
|
||||
|
||||
import br.gov.sigefp.budget.domain.BudgetAllocation;
|
||||
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.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface BudgetAllocationRepository extends JpaRepository<BudgetAllocation, UUID> {
|
||||
|
||||
List<BudgetAllocation> findByBudgetLineId(UUID budgetLineId);
|
||||
|
||||
@Query("SELECT ba FROM BudgetAllocation ba WHERE ba.budgetLine.id = :budgetLineId ORDER BY ba.createdAt DESC")
|
||||
Optional<BudgetAllocation> findLatestByBudgetLineId(@Param("budgetLineId") UUID budgetLineId);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(ba.initialAmount + ba.adjustmentAmount), 0) FROM BudgetAllocation ba WHERE ba.budgetLine.id = :budgetLineId")
|
||||
BigDecimal calculateTotalAllocatedByBudgetLineId(@Param("budgetLineId") UUID budgetLineId);
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package br.gov.sigefp.budget.repository;
|
||||
|
||||
import br.gov.sigefp.budget.domain.BudgetEntry;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface BudgetEntryRepository extends JpaRepository<BudgetEntry, UUID> {
|
||||
Page<BudgetEntry> findByBudgetLineId(UUID budgetLineId, Pageable pageable);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package br.gov.sigefp.budget.repository;
|
||||
|
||||
import br.gov.sigefp.budget.domain.BudgetExecution;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
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.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface BudgetExecutionRepository extends JpaRepository<BudgetExecution, UUID>,
|
||||
org.springframework.data.jpa.repository.JpaSpecificationExecutor<BudgetExecution> {
|
||||
|
||||
List<BudgetExecution> findByBudgetLineId(UUID budgetLineId);
|
||||
|
||||
@Query("SELECT be FROM BudgetExecution be WHERE be.budgetLine.id = :budgetLineId AND be.movementType = :movementType")
|
||||
List<BudgetExecution> findByBudgetLineIdAndMovementType(@Param("budgetLineId") UUID budgetLineId,
|
||||
@Param("movementType") String movementType);
|
||||
|
||||
@Query("SELECT be FROM BudgetExecution be WHERE be.periodId = :periodId")
|
||||
List<BudgetExecution> findByPeriodId(@Param("periodId") Long periodId);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(be.amount), 0) FROM BudgetExecution be WHERE be.budgetLine.id = :budgetLineId AND be.movementType = 'COMMITMENT'")
|
||||
BigDecimal calculateTotalCommittedByBudgetLineId(@Param("budgetLineId") UUID budgetLineId);
|
||||
|
||||
@Query("SELECT be FROM BudgetExecution be WHERE be.budgetLine.fiscalYear.id = :fiscalYearId")
|
||||
List<BudgetExecution> findExecutionsByFiscalYearId(@Param("fiscalYearId") UUID fiscalYearId);
|
||||
|
||||
@Query("SELECT be FROM BudgetExecution be WHERE " +
|
||||
"(:budgetLineId IS NULL OR be.budgetLine.id = :budgetLineId) AND " +
|
||||
"(:periodId IS NULL OR be.periodId = :periodId) AND " +
|
||||
"(:movementType IS NULL OR be.movementType = :movementType)")
|
||||
Page<BudgetExecution> findByFilters(
|
||||
@Param("budgetLineId") UUID budgetLineId,
|
||||
@Param("periodId") Long periodId,
|
||||
@Param("movementType") String movementType,
|
||||
Pageable pageable);
|
||||
|
||||
// Helper para listas sem paginação (exportação)
|
||||
@Query("SELECT be FROM BudgetExecution be WHERE " +
|
||||
"(:budgetLineId IS NULL OR be.budgetLine.id = :budgetLineId) AND " +
|
||||
"(:periodId IS NULL OR be.periodId = :periodId) AND " +
|
||||
"(:movementType IS NULL OR be.movementType = :movementType)")
|
||||
List<BudgetExecution> findByFiltersList(
|
||||
@Param("budgetLineId") UUID budgetLineId,
|
||||
@Param("periodId") Long periodId,
|
||||
@Param("movementType") String movementType);
|
||||
|
||||
@Query("SELECT COUNT(be) FROM BudgetExecution be WHERE be.budgetLine.fiscalYear.id = :fiscalYearId AND be.movementType = :movementType")
|
||||
long countByFiscalYearIdAndMovementType(@Param("fiscalYearId") UUID fiscalYearId,
|
||||
@Param("movementType") String movementType);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(be.amount), 0) FROM BudgetExecution be WHERE be.referenceId = :referenceId AND be.movementType = 'COMMITMENT'")
|
||||
BigDecimal calculateTotalCommittedByReferenceId(@Param("referenceId") UUID referenceId);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(be.amount), 0) FROM BudgetExecution be WHERE be.referenceId = :referenceId AND be.movementType = 'LIQUIDATION'")
|
||||
BigDecimal calculateTotalLiquidatedByReferenceId(@Param("referenceId") UUID referenceId);
|
||||
|
||||
@Query("SELECT COALESCE(SUM(be.amount), 0) FROM BudgetExecution be WHERE be.referenceId = :referenceId AND be.movementType = 'PAYMENT'")
|
||||
BigDecimal calculateTotalPaidByReferenceId(@Param("referenceId") UUID referenceId);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package br.gov.sigefp.budget.repository;
|
||||
|
||||
import br.gov.sigefp.budget.domain.BudgetLine;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
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.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface BudgetLineRepository extends JpaRepository<BudgetLine, UUID>, JpaSpecificationExecutor<BudgetLine> {
|
||||
|
||||
Optional<BudgetLine> findByCode(String code);
|
||||
|
||||
boolean existsByCode(String code);
|
||||
|
||||
@Query("SELECT bl FROM BudgetLine bl WHERE bl.fiscalYear.id = :fiscalYearId AND bl.code = :code")
|
||||
Optional<BudgetLine> findByFiscalYearIdAndCode(@Param("fiscalYearId") UUID fiscalYearId,
|
||||
@Param("code") String code);
|
||||
|
||||
@Query("SELECT COUNT(bl) > 0 FROM BudgetLine bl WHERE bl.fiscalYear.id = :fiscalYearId AND bl.code = :code")
|
||||
boolean existsByFiscalYearIdAndCode(@Param("fiscalYearId") UUID fiscalYearId, @Param("code") String code);
|
||||
|
||||
List<BudgetLine> findByFiscalYearId(UUID fiscalYearId);
|
||||
|
||||
@Query("SELECT bl FROM BudgetLine bl WHERE bl.fiscalYear.id = :fiscalYearId AND bl.ministry = :ministryId")
|
||||
List<BudgetLine> findByFiscalYearIdAndMinistryId(@Param("fiscalYearId") UUID fiscalYearId,
|
||||
@Param("ministryId") UUID ministryId);
|
||||
|
||||
@Query("SELECT bl FROM BudgetLine bl WHERE bl.fiscalYear.id = :fiscalYearId AND bl.orgUnit = :orgUnitId")
|
||||
List<BudgetLine> findByFiscalYearIdAndOrgUnitId(@Param("fiscalYearId") UUID fiscalYearId,
|
||||
@Param("orgUnitId") UUID orgUnitId);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package br.gov.sigefp.budget.repository;
|
||||
|
||||
import br.gov.sigefp.budget.domain.EconomicClassification;
|
||||
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 EconomicClassificationRepository
|
||||
extends JpaRepository<EconomicClassification, UUID>, JpaSpecificationExecutor<EconomicClassification> {
|
||||
Optional<EconomicClassification> findByCode(String code);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package br.gov.sigefp.budget.repository;
|
||||
|
||||
import br.gov.sigefp.budget.domain.FiscalYear;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface FiscalYearRepository extends JpaRepository<FiscalYear, UUID> {
|
||||
|
||||
Optional<FiscalYear> findByYear(Integer year);
|
||||
|
||||
boolean existsByYear(Integer year);
|
||||
|
||||
@Query("SELECT fy FROM FiscalYear fy WHERE fy.status = 'OPEN' ORDER BY fy.year DESC")
|
||||
Optional<FiscalYear> findCurrentFiscalYear();
|
||||
}
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetEntryDTO;
|
||||
import br.gov.sigefp.budget.api.dto.CreateBudgetEntryDTO;
|
||||
import br.gov.sigefp.budget.domain.BudgetAllocation;
|
||||
import br.gov.sigefp.budget.domain.BudgetEntry;
|
||||
import br.gov.sigefp.budget.domain.BudgetLine;
|
||||
import br.gov.sigefp.budget.domain.BudgetEntryType;
|
||||
import br.gov.sigefp.budget.repository.BudgetAllocationRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetEntryRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetLineRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class BudgetEntryService {
|
||||
|
||||
private final BudgetEntryRepository budgetEntryRepository;
|
||||
private final BudgetAllocationRepository budgetAllocationRepository;
|
||||
private final BudgetLineRepository budgetLineRepository;
|
||||
private final BudgetExecutionRepository budgetExecutionRepository;
|
||||
|
||||
public BudgetEntryDTO create(CreateBudgetEntryDTO dto) {
|
||||
BudgetLine budgetLine = budgetLineRepository.findById(dto.getBudgetLineId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Linha orçamentária não encontrada: " + dto.getBudgetLineId()));
|
||||
|
||||
// Validação: Data da transação deve estar dentro do exercício fiscal
|
||||
if (dto.getTransactionDate().isBefore(budgetLine.getFiscalYear().getStartDate()) ||
|
||||
dto.getTransactionDate().isAfter(budgetLine.getFiscalYear().getEndDate())) {
|
||||
throw new BusinessException(
|
||||
String.format("Data da transação (%s) deve estar dentro do exercício fiscal (%s a %s)",
|
||||
dto.getTransactionDate(),
|
||||
budgetLine.getFiscalYear().getStartDate(),
|
||||
budgetLine.getFiscalYear().getEndDate()),
|
||||
"INVALID_TRANSACTION_DATE", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Validação: TRANSFER_OUT e CANCELLATION não podem exceder saldo disponível
|
||||
if (dto.getType() == BudgetEntryType.TRANSFER_OUT || dto.getType() == BudgetEntryType.CANCELLATION) {
|
||||
BigDecimal totalAllocated = budgetAllocationRepository
|
||||
.calculateTotalAllocatedByBudgetLineId(budgetLine.getId());
|
||||
BigDecimal totalCommitted = budgetExecutionRepository
|
||||
.calculateTotalCommittedByBudgetLineId(budgetLine.getId());
|
||||
BigDecimal available = totalAllocated.subtract(totalCommitted);
|
||||
|
||||
if (dto.getAmount().compareTo(available) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Transferência/Cancelamento não pode exceder saldo disponível. Disponível: %s, Solicitado: %s",
|
||||
available, dto.getAmount()),
|
||||
"INSUFFICIENT_BALANCE", HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Create the Audit Entry
|
||||
BudgetEntry entry = BudgetEntry.builder()
|
||||
.budgetLine(budgetLine)
|
||||
.type(dto.getType())
|
||||
.amount(dto.getAmount())
|
||||
.transactionDate(dto.getTransactionDate())
|
||||
.documentReference(dto.getDocumentReference())
|
||||
.description(dto.getDescription())
|
||||
.build();
|
||||
|
||||
BudgetEntry savedEntry = budgetEntryRepository.save(entry);
|
||||
|
||||
// 2. Reflect impact in BudgetAllocation
|
||||
// We create a NEW allocation record for each entry to keep the sum correct
|
||||
// according to the repository's logic: SUM(initialAmount + adjustmentAmount)
|
||||
BigDecimal initialAmount = BigDecimal.ZERO;
|
||||
BigDecimal adjustmentAmount = BigDecimal.ZERO;
|
||||
|
||||
if (dto.getType() == BudgetEntryType.INITIAL_ALLOCATION) {
|
||||
initialAmount = dto.getAmount();
|
||||
} else {
|
||||
// For other types, it's an adjustment
|
||||
// Check if it adds or subtracts
|
||||
if (dto.getType() == BudgetEntryType.CANCELLATION || dto.getType() == BudgetEntryType.TRANSFER_OUT) {
|
||||
adjustmentAmount = dto.getAmount().negate();
|
||||
} else {
|
||||
adjustmentAmount = dto.getAmount();
|
||||
}
|
||||
}
|
||||
|
||||
BudgetAllocation allocation = BudgetAllocation.builder()
|
||||
.budgetLine(budgetLine)
|
||||
.initialAmount(initialAmount)
|
||||
.adjustmentAmount(adjustmentAmount)
|
||||
.build();
|
||||
|
||||
budgetAllocationRepository.save(allocation);
|
||||
|
||||
return toDTO(savedEntry);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<BudgetEntryDTO> findByBudgetLineId(UUID budgetLineId, Pageable pageable) {
|
||||
return budgetEntryRepository.findByBudgetLineId(budgetLineId, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
private BudgetEntryDTO toDTO(BudgetEntry entry) {
|
||||
return BudgetEntryDTO.builder()
|
||||
.id(entry.getId())
|
||||
.budgetLineId(entry.getBudgetLine().getId())
|
||||
.type(entry.getType())
|
||||
.amount(entry.getAmount())
|
||||
.transactionDate(entry.getTransactionDate())
|
||||
.documentReference(entry.getDocumentReference())
|
||||
.description(entry.getDescription())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetExecutionDTO;
|
||||
import br.gov.sigefp.budget.domain.BudgetExecution;
|
||||
import br.gov.sigefp.budget.domain.BudgetLine;
|
||||
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetLineRepository;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.InsufficientBudgetException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
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.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de execuções orçamentárias.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class BudgetExecutionService {
|
||||
|
||||
private final BudgetExecutionRepository budgetExecutionRepository;
|
||||
private final BudgetLineRepository budgetLineRepository;
|
||||
private final br.gov.sigefp.budget.repository.BudgetAllocationRepository budgetAllocationRepository;
|
||||
|
||||
public BudgetExecutionDTO registerExecution(BudgetExecutionDTO dto) {
|
||||
BudgetLine budgetLine = budgetLineRepository.findById(dto.getBudgetLineId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Linha orçamentária não encontrada: " + dto.getBudgetLineId()));
|
||||
|
||||
// Validação 1: não permitir registrar movimentos num fiscalYear fechado
|
||||
if ("CLOSED".equals(budgetLine.getFiscalYear().getStatus())) {
|
||||
throw new BusinessException("Não é possível registrar movimentos em um ano fiscal fechado",
|
||||
"FISCAL_YEAR_CLOSED", org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
// Validação 2: Coerência Governamental - Verificar saldo disponível para
|
||||
// COMMITMENT
|
||||
if ("COMMITMENT".equals(dto.getMovementType())) {
|
||||
BigDecimal totalAllocated = budgetAllocationRepository
|
||||
.calculateTotalAllocatedByBudgetLineId(budgetLine.getId());
|
||||
BigDecimal totalCommitted = budgetExecutionRepository
|
||||
.calculateTotalCommittedByBudgetLineId(budgetLine.getId());
|
||||
BigDecimal available = totalAllocated.subtract(totalCommitted);
|
||||
|
||||
if (dto.getAmount().compareTo(available) > 0) {
|
||||
throw new InsufficientBudgetException(String.format(
|
||||
"Saldo insuficiente na Linha Orçamental %s. Disponível: %s, Solicitado: %s",
|
||||
budgetLine.getCode(), available, dto.getAmount()));
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 3: Sequência obrigatória para LIQUIDATION (exige COMMITMENT)
|
||||
if ("LIQUIDATION".equals(dto.getMovementType())) {
|
||||
if (dto.getReferenceId() == null) {
|
||||
throw new BusinessException("ReferenceId é obrigatório para LIQUIDATION",
|
||||
"MISSING_REFERENCE_ID", org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
BigDecimal totalCommitted = budgetExecutionRepository
|
||||
.calculateTotalCommittedByReferenceId(dto.getReferenceId());
|
||||
|
||||
if (totalCommitted.compareTo(BigDecimal.ZERO) == 0) {
|
||||
throw new BusinessException("Não existe COMMITMENT correspondente para liquidar",
|
||||
"NO_COMMITMENT", org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
BigDecimal totalLiquidated = budgetExecutionRepository
|
||||
.calculateTotalLiquidatedByReferenceId(dto.getReferenceId());
|
||||
BigDecimal availableToLiquidate = totalCommitted.subtract(totalLiquidated);
|
||||
|
||||
if (dto.getAmount().compareTo(availableToLiquidate) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Liquidação não pode exceder empenho. Disponível: %s, Solicitado: %s",
|
||||
availableToLiquidate, dto.getAmount()),
|
||||
"INSUFFICIENT_COMMITMENT", org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
// Validação 4: Sequência obrigatória para PAYMENT (exige LIQUIDATION)
|
||||
if ("PAYMENT".equals(dto.getMovementType())) {
|
||||
if (dto.getReferenceId() == null) {
|
||||
throw new BusinessException("ReferenceId é obrigatório para PAYMENT",
|
||||
"MISSING_REFERENCE_ID", org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
BigDecimal totalLiquidated = budgetExecutionRepository
|
||||
.calculateTotalLiquidatedByReferenceId(dto.getReferenceId());
|
||||
|
||||
if (totalLiquidated.compareTo(BigDecimal.ZERO) == 0) {
|
||||
throw new BusinessException("Não existe LIQUIDATION correspondente para pagar",
|
||||
"NO_LIQUIDATION", org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
BigDecimal totalPaid = budgetExecutionRepository
|
||||
.calculateTotalPaidByReferenceId(dto.getReferenceId());
|
||||
BigDecimal availableToPay = totalLiquidated.subtract(totalPaid);
|
||||
|
||||
if (dto.getAmount().compareTo(availableToPay) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Pagamento não pode exceder liquidação. Disponível: %s, Solicitado: %s",
|
||||
availableToPay, dto.getAmount()),
|
||||
"INSUFFICIENT_LIQUIDATION", org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
BudgetExecution execution = BudgetExecution.builder()
|
||||
.budgetLine(budgetLine)
|
||||
.periodId(dto.getPeriodId())
|
||||
.movementType(dto.getMovementType())
|
||||
.amount(dto.getAmount())
|
||||
.sourceModule(dto.getSourceModule())
|
||||
.referenceId(dto.getReferenceId())
|
||||
.build();
|
||||
|
||||
BudgetExecution saved = budgetExecutionRepository.save(execution);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal calculateTotalCommittedByBudgetLineId(UUID budgetLineId) {
|
||||
return budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal calculateAvailableBalance(UUID budgetLineId) {
|
||||
// Este método será usado pelo BudgetLineService, mas pode ser chamado
|
||||
// diretamente
|
||||
// O cálculo é: totalAllocated - totalCommitted
|
||||
// Mas precisamos do BudgetAllocationRepository para isso
|
||||
// Por enquanto, retornamos apenas o comprometido
|
||||
return budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BudgetExecutionDTO> findByBudgetLineId(UUID budgetLineId) {
|
||||
return budgetExecutionRepository.findByBudgetLineId(budgetLineId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BudgetExecutionDTO> findByBudgetLineIdAndMovementType(UUID budgetLineId, String movementType) {
|
||||
return budgetExecutionRepository.findByBudgetLineIdAndMovementType(budgetLineId, movementType).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BudgetExecutionDTO> findByPeriodId(Long periodId) {
|
||||
return budgetExecutionRepository.findByPeriodId(periodId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<BudgetExecutionDTO> findByFilters(UUID budgetLineId, Long periodId, String movementType,
|
||||
Pageable pageable) {
|
||||
|
||||
org.springframework.data.jpa.domain.Specification<BudgetExecution> spec = (root, query, cb) -> {
|
||||
var predicates = new java.util.ArrayList<jakarta.persistence.criteria.Predicate>();
|
||||
|
||||
if (budgetLineId != null) {
|
||||
predicates.add(cb.equal(root.get("budgetLine").get("id"), budgetLineId));
|
||||
}
|
||||
if (periodId != null) {
|
||||
predicates.add(cb.equal(root.get("periodId"), periodId));
|
||||
}
|
||||
if (movementType != null) {
|
||||
predicates.add(cb.equal(root.get("movementType"), movementType));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
|
||||
};
|
||||
|
||||
return budgetExecutionRepository.findAll(spec, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BudgetExecutionDTO> findByFiltersList(UUID budgetLineId, Long periodId, String movementType) {
|
||||
return budgetExecutionRepository.findByFiltersList(budgetLineId, periodId, movementType).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<BudgetExecutionDTO> findAll(Pageable pageable) {
|
||||
return budgetExecutionRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
private BudgetExecutionDTO toDTO(BudgetExecution execution) {
|
||||
String typePt = translateMovementType(execution.getMovementType());
|
||||
String sourcePt = translateSourceModule(execution.getSourceModule());
|
||||
|
||||
StringBuilder desc = new StringBuilder();
|
||||
desc.append(typePt);
|
||||
|
||||
if (sourcePt != null && !sourcePt.isEmpty()) {
|
||||
desc.append(" - ").append(sourcePt);
|
||||
}
|
||||
|
||||
if (execution.getPeriodId() != null) {
|
||||
String p = execution.getPeriodId().toString();
|
||||
if (p.length() == 6) {
|
||||
desc.append(" (Ref: ").append(p.substring(4, 6)).append("/").append(p.substring(0, 4)).append(")");
|
||||
} else {
|
||||
desc.append(" (Ref: ").append(p).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
return BudgetExecutionDTO.builder()
|
||||
.id(execution.getId())
|
||||
.budgetLineId(execution.getBudgetLine().getId())
|
||||
.budgetLineCode(
|
||||
execution.getBudgetLine().getCode() + " - " + execution.getBudgetLine().getDescription())
|
||||
.periodId(execution.getPeriodId())
|
||||
.movementType(execution.getMovementType())
|
||||
.amount(execution.getAmount())
|
||||
.createdAt(execution.getCreatedAt())
|
||||
.sourceModule(execution.getSourceModule())
|
||||
.referenceId(execution.getReferenceId())
|
||||
.description(desc.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String translateMovementType(String type) {
|
||||
if (type == null)
|
||||
return "";
|
||||
return switch (type) {
|
||||
case "COMMITMENT" -> "Empenho";
|
||||
case "LIQUIDATION" -> "Liquidação";
|
||||
case "PAYMENT" -> "Pagamento";
|
||||
default -> type;
|
||||
};
|
||||
}
|
||||
|
||||
private String translateSourceModule(String module) {
|
||||
if (module == null)
|
||||
return "";
|
||||
return switch (module.toLowerCase()) {
|
||||
case "rh", "payroll" -> "Folha de Pagamento";
|
||||
case "treasury" -> "Tesouraria";
|
||||
case "budget" -> "Orçamento";
|
||||
default -> module;
|
||||
};
|
||||
}
|
||||
|
||||
private String formatCurrency(BigDecimal amount) {
|
||||
return amount != null ? amount.toString() : "0.00";
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetLineDTO;
|
||||
import br.gov.sigefp.budget.domain.BudgetLine;
|
||||
import br.gov.sigefp.budget.domain.FiscalYear;
|
||||
import br.gov.sigefp.budget.repository.BudgetAllocationRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetLineRepository;
|
||||
import br.gov.sigefp.budget.repository.FiscalYearRepository;
|
||||
import br.gov.sigefp.org.repository.MinistryRepository;
|
||||
import br.gov.sigefp.org.repository.OrgUnitRepository;
|
||||
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.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de linhas orçamentárias.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class BudgetLineService {
|
||||
|
||||
private final BudgetLineRepository budgetLineRepository;
|
||||
private final FiscalYearRepository fiscalYearRepository;
|
||||
private final MinistryRepository ministryRepository;
|
||||
private final OrgUnitRepository orgUnitRepository;
|
||||
|
||||
public BudgetLineDTO create(BudgetLineDTO dto) {
|
||||
// Validação: não permitir criar duas linhas com o mesmo code para o mesmo
|
||||
// fiscalYear
|
||||
if (budgetLineRepository.existsByFiscalYearIdAndCode(dto.getFiscalYearId(), dto.getCode())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Linha orçamentária com código %s já existe para o ano fiscal informado",
|
||||
dto.getCode()));
|
||||
}
|
||||
|
||||
FiscalYear fiscalYear = fiscalYearRepository.findById(dto.getFiscalYearId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ano fiscal não encontrado: " + dto.getFiscalYearId()));
|
||||
|
||||
// Validações cruzadas
|
||||
if (dto.getMinistryId() != null && !ministryRepository.existsById(dto.getMinistryId())) {
|
||||
throw new IllegalArgumentException("Ministério não encontrado: " + dto.getMinistryId());
|
||||
}
|
||||
if (dto.getOrgUnitId() != null && !orgUnitRepository.existsById(dto.getOrgUnitId())) {
|
||||
throw new IllegalArgumentException("Unidade organizacional não encontrada: " + dto.getOrgUnitId());
|
||||
}
|
||||
// Validação: orgUnit deve pertencer ao mesmo ministry
|
||||
if (dto.getMinistryId() != null && dto.getOrgUnitId() != null) {
|
||||
orgUnitRepository.findById(dto.getOrgUnitId())
|
||||
.ifPresent(orgUnit -> {
|
||||
if (!dto.getMinistryId().equals(orgUnit.getMinistry())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unidade organizacional deve pertencer ao mesmo ministério");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
BudgetLine budgetLine = BudgetLine.builder()
|
||||
.fiscalYear(fiscalYear)
|
||||
.code(dto.getCode())
|
||||
.description(dto.getDescription())
|
||||
.ministry(dto.getMinistryId())
|
||||
.orgUnit(dto.getOrgUnitId())
|
||||
.economicClass(dto.getEconomicClass())
|
||||
.build();
|
||||
|
||||
BudgetLine saved = budgetLineRepository.save(budgetLine);
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public BudgetLineDTO update(UUID id, BudgetLineDTO dto) {
|
||||
BudgetLine budgetLine = budgetLineRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Linha orçamentária não encontrada: " + id));
|
||||
|
||||
if (dto.getCode() != null && !dto.getCode().equals(budgetLine.getCode())) {
|
||||
if (budgetLineRepository.existsByFiscalYearIdAndCode(budgetLine.getFiscalYear().getId(), dto.getCode())) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Linha orçamentária com código %s já existe para o ano fiscal", dto.getCode()));
|
||||
}
|
||||
budgetLine.setCode(dto.getCode());
|
||||
}
|
||||
|
||||
if (dto.getDescription() != null) {
|
||||
budgetLine.setDescription(dto.getDescription());
|
||||
}
|
||||
if (dto.getMinistryId() != null) {
|
||||
if (!ministryRepository.existsById(dto.getMinistryId())) {
|
||||
throw new IllegalArgumentException("Ministério não encontrado: " + dto.getMinistryId());
|
||||
}
|
||||
budgetLine.setMinistry(dto.getMinistryId());
|
||||
}
|
||||
if (dto.getOrgUnitId() != null) {
|
||||
if (!orgUnitRepository.existsById(dto.getOrgUnitId())) {
|
||||
throw new IllegalArgumentException("Unidade organizacional não encontrada: " + dto.getOrgUnitId());
|
||||
}
|
||||
// Validação: orgUnit deve pertencer ao mesmo ministry
|
||||
orgUnitRepository.findById(dto.getOrgUnitId())
|
||||
.ifPresent(orgUnit -> {
|
||||
UUID currentMinistryId = budgetLine.getMinistry();
|
||||
if (currentMinistryId != null && !currentMinistryId.equals(orgUnit.getMinistry())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unidade organizacional deve pertencer ao mesmo ministério");
|
||||
}
|
||||
});
|
||||
budgetLine.setOrgUnit(dto.getOrgUnitId());
|
||||
}
|
||||
if (dto.getEconomicClass() != null) {
|
||||
budgetLine.setEconomicClass(dto.getEconomicClass());
|
||||
}
|
||||
|
||||
BudgetLine saved = budgetLineRepository.save(budgetLine);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BudgetLineDTO findById(UUID id) {
|
||||
BudgetLine budgetLine = budgetLineRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Linha orçamentária não encontrada: " + id));
|
||||
return toDTO(budgetLine);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<BudgetLineDTO> findAll(Pageable pageable) {
|
||||
return budgetLineRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BudgetLineDTO> findByFiscalYearId(UUID fiscalYearId) {
|
||||
return budgetLineRepository.findByFiscalYearId(fiscalYearId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<BudgetLineDTO> findWithFilters(UUID fiscalYearId, UUID ministryId, UUID orgUnitId, String search,
|
||||
Pageable pageable) {
|
||||
return budgetLineRepository.findAll((root, query, cb) -> {
|
||||
var predicates = new java.util.ArrayList<jakarta.persistence.criteria.Predicate>();
|
||||
|
||||
if (fiscalYearId != null) {
|
||||
predicates.add(cb.equal(root.get("fiscalYear").get("id"), fiscalYearId));
|
||||
}
|
||||
|
||||
if (ministryId != null) {
|
||||
predicates.add(cb.equal(root.get("ministry"), ministryId));
|
||||
}
|
||||
|
||||
if (orgUnitId != null) {
|
||||
predicates.add(cb.equal(root.get("orgUnit"), orgUnitId));
|
||||
}
|
||||
|
||||
if (search != null && !search.trim().isEmpty()) {
|
||||
String likePattern = "%" + search.trim().toLowerCase() + "%";
|
||||
predicates.add(cb.or(
|
||||
cb.like(cb.lower(root.get("code")), likePattern),
|
||||
cb.like(cb.lower(root.get("description")), likePattern),
|
||||
cb.like(cb.lower(root.get("economicClass")), likePattern)));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
|
||||
}, pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
private BudgetLineDTO toDTO(BudgetLine budgetLine) {
|
||||
// Valores calculados via @Formula no carregamento da entidade (Performance N+2
|
||||
// fix)
|
||||
BigDecimal totalAllocated = budgetLine.getTotalAllocated() != null ? budgetLine.getTotalAllocated()
|
||||
: BigDecimal.ZERO;
|
||||
BigDecimal totalCommitted = budgetLine.getTotalCommitted() != null ? budgetLine.getTotalCommitted()
|
||||
: BigDecimal.ZERO;
|
||||
BigDecimal availableBalance = totalAllocated.subtract(totalCommitted);
|
||||
|
||||
return BudgetLineDTO.builder()
|
||||
.id(budgetLine.getId())
|
||||
.fiscalYearId(budgetLine.getFiscalYear().getId())
|
||||
.code(budgetLine.getCode())
|
||||
.description(budgetLine.getDescription())
|
||||
.ministryId(budgetLine.getMinistry())
|
||||
.orgUnitId(budgetLine.getOrgUnit())
|
||||
.economicClass(budgetLine.getEconomicClass())
|
||||
.totalAllocated(totalAllocated)
|
||||
.totalCommitted(totalCommitted)
|
||||
.availableBalance(availableBalance)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.EconomicClassificationDTO;
|
||||
import br.gov.sigefp.budget.domain.EconomicClassification;
|
||||
import br.gov.sigefp.budget.repository.EconomicClassificationRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EconomicClassificationService {
|
||||
|
||||
private final EconomicClassificationRepository repository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<EconomicClassificationDTO> findAll(String type) {
|
||||
List<EconomicClassification> list;
|
||||
if (type != null && !type.isEmpty()) {
|
||||
// Using simple findAll and filtering in memory for small list (20-100 items)
|
||||
// Or could use Specification if needed. Current volume is small.
|
||||
list = repository.findAll().stream()
|
||||
.filter(e -> e.getType().equalsIgnoreCase(type))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
list = repository.findAll();
|
||||
}
|
||||
|
||||
return list.stream()
|
||||
.sorted(Comparator.comparing(EconomicClassification::getCode))
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private EconomicClassificationDTO toDTO(EconomicClassification entity) {
|
||||
return EconomicClassificationDTO.builder()
|
||||
.id(entity.getId())
|
||||
.code(entity.getCode())
|
||||
.description(entity.getDescription())
|
||||
.type(entity.getType())
|
||||
.uemoaCode(entity.getUemoaCode())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.FiscalYearDTO;
|
||||
import br.gov.sigefp.budget.domain.BudgetExecution;
|
||||
import br.gov.sigefp.budget.domain.FiscalYear;
|
||||
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
|
||||
import br.gov.sigefp.budget.repository.FiscalYearRepository;
|
||||
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.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de anos fiscais.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class FiscalYearService {
|
||||
|
||||
private final FiscalYearRepository fiscalYearRepository;
|
||||
private final BudgetExecutionRepository budgetExecutionRepository;
|
||||
|
||||
public FiscalYearDTO create(FiscalYearDTO dto) {
|
||||
if (fiscalYearRepository.existsByYear(dto.getYear())) {
|
||||
throw new IllegalArgumentException("Ano fiscal já existe: " + dto.getYear());
|
||||
}
|
||||
|
||||
FiscalYear fiscalYear = FiscalYear.builder()
|
||||
.year(dto.getYear())
|
||||
.startDate(dto.getStartDate())
|
||||
.endDate(dto.getEndDate())
|
||||
.status(dto.getStatus() != null ? dto.getStatus() : "DRAFT")
|
||||
.build();
|
||||
|
||||
FiscalYear saved = fiscalYearRepository.save(fiscalYear);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public FiscalYearDTO open(UUID id) {
|
||||
FiscalYear fiscalYear = fiscalYearRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ano fiscal não encontrado: " + id));
|
||||
|
||||
fiscalYear.setStatus("OPEN");
|
||||
FiscalYear saved = fiscalYearRepository.save(fiscalYear);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public FiscalYearDTO close(UUID id) {
|
||||
FiscalYear fiscalYear = fiscalYearRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ano fiscal não encontrado: " + id));
|
||||
|
||||
// Validação: não permitir fechar se existirem movimentos em aberto
|
||||
// Verificar se há execuções pendentes (COMMITMENT) de forma performática
|
||||
// (COUNT)
|
||||
long pendingCommitments = budgetExecutionRepository.countByFiscalYearIdAndMovementType(id, "COMMITMENT");
|
||||
|
||||
if (pendingCommitments > 0) {
|
||||
throw new IllegalArgumentException("Não é possível fechar o ano fiscal: existem movimentos em aberto");
|
||||
}
|
||||
|
||||
fiscalYear.setStatus("CLOSED");
|
||||
FiscalYear saved = fiscalYearRepository.save(fiscalYear);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FiscalYearDTO findById(UUID id) {
|
||||
FiscalYear fiscalYear = fiscalYearRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Ano fiscal não encontrado: " + id));
|
||||
return toDTO(fiscalYear);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<FiscalYearDTO> findAll(Pageable pageable) {
|
||||
return fiscalYearRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FiscalYearDTO getCurrentFiscalYear() {
|
||||
FiscalYear fiscalYear = fiscalYearRepository.findCurrentFiscalYear()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Não existe ano fiscal aberto"));
|
||||
return toDTO(fiscalYear);
|
||||
}
|
||||
|
||||
private FiscalYearDTO toDTO(FiscalYear fiscalYear) {
|
||||
return FiscalYearDTO.builder()
|
||||
.id(fiscalYear.getId())
|
||||
.year(fiscalYear.getYear())
|
||||
.startDate(fiscalYear.getStartDate())
|
||||
.endDate(fiscalYear.getEndDate())
|
||||
.status(fiscalYear.getStatus())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetExecutionDTO;
|
||||
import br.gov.sigefp.budget.domain.BudgetExecution;
|
||||
import br.gov.sigefp.budget.domain.BudgetLine;
|
||||
import br.gov.sigefp.budget.domain.FiscalYear;
|
||||
import br.gov.sigefp.budget.repository.BudgetAllocationRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
|
||||
import br.gov.sigefp.budget.repository.BudgetLineRepository;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.InsufficientBudgetException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
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.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BudgetExecutionServiceTest {
|
||||
|
||||
@Mock
|
||||
private BudgetExecutionRepository budgetExecutionRepository;
|
||||
|
||||
@Mock
|
||||
private BudgetLineRepository budgetLineRepository;
|
||||
|
||||
@Mock
|
||||
private BudgetAllocationRepository budgetAllocationRepository;
|
||||
|
||||
@InjectMocks
|
||||
private BudgetExecutionService budgetExecutionService;
|
||||
|
||||
private UUID budgetLineId;
|
||||
private BudgetLine budgetLine;
|
||||
private FiscalYear fiscalYear;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
budgetLineId = UUID.randomUUID();
|
||||
fiscalYear = FiscalYear.builder()
|
||||
.year(2024)
|
||||
.status("OPEN")
|
||||
.build();
|
||||
|
||||
budgetLine = BudgetLine.builder()
|
||||
.id(budgetLineId)
|
||||
.code("01.01.01")
|
||||
.fiscalYear(fiscalYear)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve registrar execução com sucesso quando há saldo")
|
||||
void registerExecution_Success() {
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("COMMITMENT")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.periodId(202401L)
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetAllocationRepository.calculateTotalAllocatedByBudgetLineId(budgetLineId))
|
||||
.thenReturn(new BigDecimal("5000"));
|
||||
when(budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId))
|
||||
.thenReturn(new BigDecimal("2000"));
|
||||
|
||||
when(budgetExecutionRepository.save(any(BudgetExecution.class))).thenAnswer(i -> {
|
||||
BudgetExecution e = i.getArgument(0);
|
||||
e.setId(UUID.randomUUID());
|
||||
return e;
|
||||
});
|
||||
|
||||
BudgetExecutionDTO result = budgetExecutionService.registerExecution(dto);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(new BigDecimal("1000"), result.getAmount());
|
||||
verify(budgetExecutionRepository).save(any(BudgetExecution.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao registrar COMMITMENT se não houver saldo disponível")
|
||||
void registerExecution_InsufficientFunds() {
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("COMMITMENT")
|
||||
.amount(new BigDecimal("4000"))
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetAllocationRepository.calculateTotalAllocatedByBudgetLineId(budgetLineId))
|
||||
.thenReturn(new BigDecimal("5000"));
|
||||
when(budgetExecutionRepository.calculateTotalCommittedByBudgetLineId(budgetLineId))
|
||||
.thenReturn(new BigDecimal("2000")); // Disponível = 3000
|
||||
|
||||
InsufficientBudgetException exception = assertThrows(InsufficientBudgetException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Saldo insuficiente"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao registrar execução em exercício fiscal fechado")
|
||||
void registerExecution_ClosedYear() {
|
||||
fiscalYear.setStatus("CLOSED");
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertEquals("Não é possível registrar movimentos em um ano fiscal fechado", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao criar LIQUIDATION sem COMMITMENT correspondente")
|
||||
void registerExecution_LiquidationWithoutCommitment() {
|
||||
UUID referenceId = UUID.randomUUID();
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("LIQUIDATION")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.referenceId(referenceId)
|
||||
.periodId(202401L)
|
||||
.sourceModule("rh")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetExecutionRepository.calculateTotalCommittedByReferenceId(referenceId))
|
||||
.thenReturn(BigDecimal.ZERO); // Sem COMMITMENT
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Não existe COMMITMENT correspondente"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao criar LIQUIDATION sem referenceId")
|
||||
void registerExecution_LiquidationWithoutReferenceId() {
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("LIQUIDATION")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.referenceId(null) // Sem referenceId
|
||||
.periodId(202401L)
|
||||
.sourceModule("rh")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("ReferenceId é obrigatório"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao criar LIQUIDATION excedendo COMMITMENT")
|
||||
void registerExecution_LiquidationExceedingCommitment() {
|
||||
UUID referenceId = UUID.randomUUID();
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("LIQUIDATION")
|
||||
.amount(new BigDecimal("2000"))
|
||||
.referenceId(referenceId)
|
||||
.periodId(202401L)
|
||||
.sourceModule("rh")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetExecutionRepository.calculateTotalCommittedByReferenceId(referenceId))
|
||||
.thenReturn(new BigDecimal("1000")); // COMMITMENT de 1000
|
||||
when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
|
||||
.thenReturn(BigDecimal.ZERO); // Nenhuma liquidação ainda
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Liquidação não pode exceder empenho"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao criar PAYMENT sem LIQUIDATION correspondente")
|
||||
void registerExecution_PaymentWithoutLiquidation() {
|
||||
UUID referenceId = UUID.randomUUID();
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("PAYMENT")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.referenceId(referenceId)
|
||||
.periodId(202401L)
|
||||
.sourceModule("treasury")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
|
||||
.thenReturn(BigDecimal.ZERO); // Sem LIQUIDATION
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Não existe LIQUIDATION correspondente"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao criar PAYMENT sem referenceId")
|
||||
void registerExecution_PaymentWithoutReferenceId() {
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("PAYMENT")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.referenceId(null) // Sem referenceId
|
||||
.periodId(202401L)
|
||||
.sourceModule("treasury")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("ReferenceId é obrigatório"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar ao criar PAYMENT excedendo LIQUIDATION")
|
||||
void registerExecution_PaymentExceedingLiquidation() {
|
||||
UUID referenceId = UUID.randomUUID();
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("PAYMENT")
|
||||
.amount(new BigDecimal("2000"))
|
||||
.referenceId(referenceId)
|
||||
.periodId(202401L)
|
||||
.sourceModule("treasury")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
|
||||
.thenReturn(new BigDecimal("1000")); // LIQUIDATION de 1000
|
||||
when(budgetExecutionRepository.calculateTotalPaidByReferenceId(referenceId))
|
||||
.thenReturn(BigDecimal.ZERO); // Nenhum pagamento ainda
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> budgetExecutionService.registerExecution(dto));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Pagamento não pode exceder liquidação"));
|
||||
verify(budgetExecutionRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve registrar LIQUIDATION com sucesso quando há COMMITMENT correspondente")
|
||||
void registerExecution_LiquidationSuccess() {
|
||||
UUID referenceId = UUID.randomUUID();
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("LIQUIDATION")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.referenceId(referenceId)
|
||||
.periodId(202401L)
|
||||
.sourceModule("rh")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetExecutionRepository.calculateTotalCommittedByReferenceId(referenceId))
|
||||
.thenReturn(new BigDecimal("2000")); // COMMITMENT de 2000
|
||||
when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
|
||||
.thenReturn(BigDecimal.ZERO); // Nenhuma liquidação ainda
|
||||
|
||||
when(budgetExecutionRepository.save(any(BudgetExecution.class))).thenAnswer(i -> {
|
||||
BudgetExecution e = i.getArgument(0);
|
||||
e.setId(UUID.randomUUID());
|
||||
return e;
|
||||
});
|
||||
|
||||
BudgetExecutionDTO result = budgetExecutionService.registerExecution(dto);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(new BigDecimal("1000"), result.getAmount());
|
||||
assertEquals("LIQUIDATION", result.getMovementType());
|
||||
verify(budgetExecutionRepository).save(any(BudgetExecution.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve registrar PAYMENT com sucesso quando há LIQUIDATION correspondente")
|
||||
void registerExecution_PaymentSuccess() {
|
||||
UUID referenceId = UUID.randomUUID();
|
||||
BudgetExecutionDTO dto = BudgetExecutionDTO.builder()
|
||||
.budgetLineId(budgetLineId)
|
||||
.movementType("PAYMENT")
|
||||
.amount(new BigDecimal("1000"))
|
||||
.referenceId(referenceId)
|
||||
.periodId(202401L)
|
||||
.sourceModule("treasury")
|
||||
.build();
|
||||
|
||||
when(budgetLineRepository.findById(budgetLineId)).thenReturn(Optional.of(budgetLine));
|
||||
when(budgetExecutionRepository.calculateTotalLiquidatedByReferenceId(referenceId))
|
||||
.thenReturn(new BigDecimal("2000")); // LIQUIDATION de 2000
|
||||
when(budgetExecutionRepository.calculateTotalPaidByReferenceId(referenceId))
|
||||
.thenReturn(BigDecimal.ZERO); // Nenhum pagamento ainda
|
||||
|
||||
when(budgetExecutionRepository.save(any(BudgetExecution.class))).thenAnswer(i -> {
|
||||
BudgetExecution e = i.getArgument(0);
|
||||
e.setId(UUID.randomUUID());
|
||||
return e;
|
||||
});
|
||||
|
||||
BudgetExecutionDTO result = budgetExecutionService.registerExecution(dto);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(new BigDecimal("1000"), result.getAmount());
|
||||
assertEquals("PAYMENT", result.getMovementType());
|
||||
verify(budgetExecutionRepository).save(any(BudgetExecution.class));
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.BudgetLineDTO;
|
||||
import br.gov.sigefp.budget.domain.BudgetLine;
|
||||
import br.gov.sigefp.budget.domain.FiscalYear;
|
||||
import br.gov.sigefp.budget.repository.BudgetLineRepository;
|
||||
import br.gov.sigefp.budget.repository.FiscalYearRepository;
|
||||
import br.gov.sigefp.org.repository.MinistryRepository;
|
||||
import br.gov.sigefp.org.repository.OrgUnitRepository;
|
||||
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.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class BudgetLineServiceTest {
|
||||
|
||||
@Mock
|
||||
private BudgetLineRepository budgetLineRepository;
|
||||
@Mock
|
||||
private FiscalYearRepository fiscalYearRepository;
|
||||
@Mock
|
||||
private MinistryRepository ministryRepository;
|
||||
@Mock
|
||||
private OrgUnitRepository orgUnitRepository;
|
||||
|
||||
@InjectMocks
|
||||
private BudgetLineService budgetLineService;
|
||||
|
||||
private BudgetLine budgetLine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
FiscalYear fiscalYear = FiscalYear.builder().year(2025).status("OPEN").build();
|
||||
fiscalYear.setId(UUID.randomUUID());
|
||||
|
||||
budgetLine = BudgetLine.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.code("01.01")
|
||||
.description("Teste")
|
||||
.fiscalYear(fiscalYear)
|
||||
.totalAllocated(new BigDecimal("1000"))
|
||||
.totalCommitted(new BigDecimal("200"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should use pre-calculated Formula fields for DTO conversion")
|
||||
void toDTO_ShouldUseFormulaFields() {
|
||||
when(budgetLineRepository.findById(budgetLine.getId())).thenReturn(Optional.of(budgetLine));
|
||||
|
||||
BudgetLineDTO dto = budgetLineService.findById(budgetLine.getId());
|
||||
|
||||
assertNotNull(dto);
|
||||
assertEquals(new BigDecimal("1000"), dto.getTotalAllocated());
|
||||
assertEquals(new BigDecimal("200"), dto.getTotalCommitted());
|
||||
assertEquals(new BigDecimal("800"), dto.getAvailableBalance());
|
||||
// Verify no extra repository calls are made (implicitly verified by lack of
|
||||
// mocks for them)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package br.gov.sigefp.budget.service;
|
||||
|
||||
import br.gov.sigefp.budget.api.dto.FiscalYearDTO;
|
||||
import br.gov.sigefp.budget.domain.FiscalYear;
|
||||
import br.gov.sigefp.budget.repository.BudgetExecutionRepository;
|
||||
import br.gov.sigefp.budget.repository.FiscalYearRepository;
|
||||
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.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FiscalYearServiceTest {
|
||||
|
||||
@Mock
|
||||
private FiscalYearRepository fiscalYearRepository;
|
||||
@Mock
|
||||
private BudgetExecutionRepository budgetExecutionRepository;
|
||||
|
||||
@InjectMocks
|
||||
private FiscalYearService fiscalYearService;
|
||||
|
||||
@Test
|
||||
@DisplayName("Should use efficient COUNT query to check pending commitments")
|
||||
void close_ShouldUseCountQuery() {
|
||||
UUID id = UUID.randomUUID();
|
||||
FiscalYear fiscalYear = FiscalYear.builder().id(id).status("OPEN").build();
|
||||
|
||||
when(fiscalYearRepository.findById(id)).thenReturn(Optional.of(fiscalYear));
|
||||
when(budgetExecutionRepository.countByFiscalYearIdAndMovementType(id, "COMMITMENT")).thenReturn(0L); // No
|
||||
// pending
|
||||
when(fiscalYearRepository.save(any(FiscalYear.class))).thenReturn(fiscalYear);
|
||||
|
||||
fiscalYearService.close(id);
|
||||
|
||||
verify(budgetExecutionRepository).countByFiscalYearIdAndMovementType(id, "COMMITMENT");
|
||||
verify(budgetExecutionRepository, never()).findExecutionsByFiscalYearId(any()); // Ensure list is NOT loaded
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should block closing if pending commitments exist")
|
||||
void close_ShouldBlockIfPending() {
|
||||
UUID id = UUID.randomUUID();
|
||||
FiscalYear fiscalYear = FiscalYear.builder().id(id).status("OPEN").build();
|
||||
|
||||
when(fiscalYearRepository.findById(id)).thenReturn(Optional.of(fiscalYear));
|
||||
when(budgetExecutionRepository.countByFiscalYearIdAndMovementType(id, "COMMITMENT")).thenReturn(5L); // 5
|
||||
// pending
|
||||
|
||||
Exception exception = assertThrows(IllegalArgumentException.class, () -> fiscalYearService.close(id));
|
||||
|
||||
assertTrue(exception.getMessage().contains("existem movimentos em aberto"));
|
||||
verify(fiscalYearRepository, never()).save(any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user