feat: otimização de performance e ajustes finais
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
<?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-treasury</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>SIGEFP Treasury</name>
|
||||
<description>Módulo de tesouraria/pagamentos</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-budget</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>br.gov.sigefp</groupId>
|
||||
<artifactId>sigefp-rh</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.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.BankReconciliationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateBankReconciliationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.ReconciliationItemDTO;
|
||||
import br.gov.sigefp.treasury.service.BankReconciliationService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/reconciliations")
|
||||
@RequiredArgsConstructor
|
||||
public class BankReconciliationController {
|
||||
|
||||
private final BankReconciliationService reconciliationService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<BankReconciliationDTO> importStatement(
|
||||
@Valid @RequestBody CreateBankReconciliationDTO dto) {
|
||||
BankReconciliationDTO created = reconciliationService.importStatement(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reconcile")
|
||||
public ResponseEntity<BankReconciliationDTO> reconcile(@PathVariable UUID id) {
|
||||
BankReconciliationDTO updated = reconciliationService.reconcile(id);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/match-item")
|
||||
public ResponseEntity<Void> matchItem(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam UUID itemId,
|
||||
@RequestParam UUID systemTransactionId) {
|
||||
reconciliationService.matchItem(id, itemId, systemTransactionId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/finalize")
|
||||
public ResponseEntity<BankReconciliationDTO> finalize(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam UUID reconciledBy) {
|
||||
BankReconciliationDTO updated = reconciliationService.finalize(id, reconciledBy);
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<BankReconciliationDTO> findById(@PathVariable UUID id) {
|
||||
BankReconciliationDTO reconciliation = reconciliationService.findById(id);
|
||||
return ResponseEntity.ok(reconciliation);
|
||||
}
|
||||
|
||||
@GetMapping("/cash-account/{cashAccountId}")
|
||||
public ResponseEntity<List<BankReconciliationDTO>> findByCashAccountId(
|
||||
@PathVariable UUID cashAccountId) {
|
||||
List<BankReconciliationDTO> reconciliations = reconciliationService.findByCashAccountId(cashAccountId);
|
||||
return ResponseEntity.ok(reconciliations);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/unmatched-items")
|
||||
public ResponseEntity<List<ReconciliationItemDTO>> findUnmatchedItems(
|
||||
@PathVariable UUID id) {
|
||||
List<ReconciliationItemDTO> items = reconciliationService.findUnmatchedItems(id);
|
||||
return ResponseEntity.ok(items);
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CashAccountDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateCashAccountDTO;
|
||||
import br.gov.sigefp.treasury.service.CashAccountService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/cash-accounts")
|
||||
@RequiredArgsConstructor
|
||||
public class CashAccountController {
|
||||
|
||||
private final CashAccountService cashAccountService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<CashAccountDTO> create(@Valid @RequestBody CreateCashAccountDTO dto) {
|
||||
CashAccountDTO created = cashAccountService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<CashAccountDTO> findById(@PathVariable UUID id) {
|
||||
CashAccountDTO account = cashAccountService.findById(id);
|
||||
return ResponseEntity.ok(account);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<CashAccountDTO>> findAll(
|
||||
@RequestParam(name = "type", required = false) String type,
|
||||
@RequestParam(name = "activeOnly", required = false, defaultValue = "true") Boolean activeOnly) {
|
||||
List<CashAccountDTO> accounts;
|
||||
if (type != null) {
|
||||
accounts = cashAccountService.findByType(type);
|
||||
} else if (activeOnly) {
|
||||
accounts = cashAccountService.findActive();
|
||||
} else {
|
||||
accounts = cashAccountService.findAll();
|
||||
}
|
||||
return ResponseEntity.ok(accounts);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/available-balance")
|
||||
public ResponseEntity<BigDecimal> getAvailableBalance(@PathVariable UUID id) {
|
||||
BigDecimal balance = cashAccountService.getAvailableBalance(id);
|
||||
return ResponseEntity.ok(balance);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/current-balance")
|
||||
public ResponseEntity<BigDecimal> getCurrentBalance(@PathVariable UUID id) {
|
||||
BigDecimal balance = cashAccountService.getCurrentBalance(id);
|
||||
return ResponseEntity.ok(balance);
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CashFlowDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateCashFlowDTO;
|
||||
import br.gov.sigefp.treasury.service.CashFlowService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/cash-flow")
|
||||
@RequiredArgsConstructor
|
||||
public class CashFlowController {
|
||||
|
||||
private final CashFlowService cashFlowService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<CashFlowDTO> registerFlow(@Valid @RequestBody CreateCashFlowDTO dto) {
|
||||
CashFlowDTO created = cashFlowService.registerFlow(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<CashFlowDTO> findById(@PathVariable UUID id) {
|
||||
CashFlowDTO cashFlow = cashFlowService.findById(id);
|
||||
return ResponseEntity.ok(cashFlow);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<CashFlowDTO>> findByCashAccount(
|
||||
@RequestParam UUID cashAccountId,
|
||||
@RequestParam(required = false) LocalDate startDate,
|
||||
@RequestParam(required = false) LocalDate endDate,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(defaultValue = "transactionDate") String sortBy,
|
||||
@RequestParam(defaultValue = "DESC") String sortDirection) {
|
||||
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
Page<CashFlowDTO> result;
|
||||
if (startDate != null && endDate != null) {
|
||||
result = cashFlowService.findByCashAccountIdAndDateRange(
|
||||
cashAccountId, startDate, endDate, pageable);
|
||||
} else {
|
||||
result = cashFlowService.findByCashAccountId(cashAccountId, pageable);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{cashAccountId}/projected-balance")
|
||||
public ResponseEntity<BigDecimal> calculateProjectedBalance(
|
||||
@PathVariable UUID cashAccountId,
|
||||
@RequestParam LocalDate targetDate) {
|
||||
BigDecimal balance = cashFlowService.calculateProjectedBalance(cashAccountId, targetDate);
|
||||
return ResponseEntity.ok(balance);
|
||||
}
|
||||
|
||||
@GetMapping("/{cashAccountId}/summary")
|
||||
public ResponseEntity<Map<String, BigDecimal>> getFlowSummary(
|
||||
@PathVariable UUID cashAccountId,
|
||||
@RequestParam LocalDate startDate,
|
||||
@RequestParam LocalDate endDate) {
|
||||
Map<String, BigDecimal> summary = cashFlowService.getFlowSummary(
|
||||
cashAccountId, startDate, endDate);
|
||||
return ResponseEntity.ok(summary);
|
||||
}
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.ApprovePaymentDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentAuthorizationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentAuthorizationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.RejectPaymentDTO;
|
||||
import br.gov.sigefp.treasury.service.PaymentAuthorizationService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/authorizations")
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentAuthorizationController {
|
||||
|
||||
private final PaymentAuthorizationService authorizationService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PaymentAuthorizationDTO> requestAuthorization(
|
||||
@Valid @RequestBody CreatePaymentAuthorizationDTO dto) {
|
||||
PaymentAuthorizationDTO created = authorizationService.requestAuthorization(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public ResponseEntity<PaymentAuthorizationDTO> approve(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody ApprovePaymentDTO dto) {
|
||||
PaymentAuthorizationDTO updated = authorizationService.approve(
|
||||
id, dto.getApproverId(), dto.getComments());
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public ResponseEntity<PaymentAuthorizationDTO> reject(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody RejectPaymentDTO dto) {
|
||||
PaymentAuthorizationDTO updated = authorizationService.reject(
|
||||
id, dto.getApproverId(), dto.getReason());
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PaymentAuthorizationDTO> findById(@PathVariable UUID id) {
|
||||
PaymentAuthorizationDTO authorization = authorizationService.findById(id);
|
||||
return ResponseEntity.ok(authorization);
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ResponseEntity<List<PaymentAuthorizationDTO>> findPendingApprovals(
|
||||
@RequestParam(name = "approverId") UUID approverId) {
|
||||
List<PaymentAuthorizationDTO> authorizations = authorizationService.findPendingApprovals(approverId);
|
||||
return ResponseEntity.ok(authorizations);
|
||||
}
|
||||
|
||||
@GetMapping("/payment-order/{paymentOrderId}")
|
||||
public ResponseEntity<List<PaymentAuthorizationDTO>> findByPaymentOrderId(
|
||||
@PathVariable UUID paymentOrderId) {
|
||||
List<PaymentAuthorizationDTO> authorizations = authorizationService.findByPaymentOrderId(paymentOrderId);
|
||||
return ResponseEntity.ok(authorizations);
|
||||
}
|
||||
|
||||
@GetMapping("/payment-batch/{paymentBatchId}")
|
||||
public ResponseEntity<List<PaymentAuthorizationDTO>> findByPaymentBatchId(
|
||||
@PathVariable UUID paymentBatchId) {
|
||||
List<PaymentAuthorizationDTO> authorizations = authorizationService.findByPaymentBatchId(paymentBatchId);
|
||||
return ResponseEntity.ok(authorizations);
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentBatchDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentBatchDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.UpdateStatusDTO;
|
||||
import br.gov.sigefp.treasury.service.PaymentBatchService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para gestão de lotes de pagamento.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/payment-batches")
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentBatchController {
|
||||
|
||||
private final PaymentBatchService paymentBatchService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(name = "periodId", required = false) Long periodId,
|
||||
@RequestParam(name = "ministryId", required = false) UUID ministryId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@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) {
|
||||
|
||||
// Se há filtros, retornar lista (sem paginação)
|
||||
if (periodId != null || ministryId != null || status != null) {
|
||||
List<PaymentBatchDTO> result;
|
||||
if (periodId != null && ministryId != null) {
|
||||
result = paymentBatchService.findByPeriodIdAndMinistryId(periodId, ministryId);
|
||||
if (status != null) {
|
||||
result = result.stream()
|
||||
.filter(pb -> status.equals(pb.getStatus()))
|
||||
.toList();
|
||||
}
|
||||
} else if (periodId != null && status != null) {
|
||||
result = paymentBatchService.findByPeriodIdAndStatus(periodId, status);
|
||||
} else if (ministryId != null && status != null) {
|
||||
result = paymentBatchService.findByMinistryIdAndStatus(ministryId, status);
|
||||
} else if (periodId != null) {
|
||||
result = paymentBatchService.findByPeriodId(periodId);
|
||||
} else if (ministryId != null) {
|
||||
result = paymentBatchService.findByMinistryId(ministryId);
|
||||
} else {
|
||||
result = paymentBatchService.findByStatus(status);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// Sem filtros, retornar paginado
|
||||
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<PaymentBatchDTO> result = paymentBatchService.findAll(pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PaymentBatchDTO> findById(@PathVariable UUID id) {
|
||||
try {
|
||||
PaymentBatchDTO dto = paymentBatchService.findById(id);
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PaymentBatchDTO> create(@Valid @RequestBody CreatePaymentBatchDTO dto) {
|
||||
try {
|
||||
PaymentBatchDTO created = paymentBatchService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/status")
|
||||
public ResponseEntity<PaymentBatchDTO> updateStatus(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody UpdateStatusDTO dto) {
|
||||
try {
|
||||
PaymentBatchDTO updated = paymentBatchService.updateStatus(id, dto.getStatus());
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentOrderDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.GenerateOrdersFromPayrollRunDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentOrderDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.UpdateStatusDTO;
|
||||
import br.gov.sigefp.treasury.service.PaymentOrderService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para gestão de ordens de pagamento.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/payment-orders")
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentOrderController {
|
||||
|
||||
private final PaymentOrderService paymentOrderService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(name = "batchId", required = false) UUID batchId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@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) {
|
||||
|
||||
// Se há filtros, retornar lista (sem paginação)
|
||||
if (batchId != null || status != null) {
|
||||
List<PaymentOrderDTO> result;
|
||||
if (batchId != null && status != null) {
|
||||
result = paymentOrderService.findByPaymentBatchIdAndStatus(batchId, status);
|
||||
} else if (batchId != null) {
|
||||
result = paymentOrderService.findByPaymentBatchId(batchId);
|
||||
} else {
|
||||
result = paymentOrderService.findByStatus(status);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// Sem filtros, retornar paginado
|
||||
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<PaymentOrderDTO> result = paymentOrderService.findAll(pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PaymentOrderDTO> findById(@PathVariable UUID id) {
|
||||
try {
|
||||
PaymentOrderDTO dto = paymentOrderService.findById(id);
|
||||
return ResponseEntity.ok(dto);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PaymentOrderDTO> create(@Valid @RequestBody CreatePaymentOrderDTO dto) {
|
||||
try {
|
||||
PaymentOrderDTO created = paymentOrderService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/status")
|
||||
public ResponseEntity<PaymentOrderDTO> updateStatus(
|
||||
@PathVariable UUID id,
|
||||
@Valid @RequestBody UpdateStatusDTO dto) {
|
||||
try {
|
||||
PaymentOrderDTO updated = paymentOrderService.updateStatus(id, dto.getStatus());
|
||||
return ResponseEntity.ok(updated);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar ordens de pagamento a partir de uma execução de folha.
|
||||
* POST /api/treasury/payment-orders/generate-from-payroll
|
||||
*/
|
||||
@PostMapping("/generate-from-payroll")
|
||||
public ResponseEntity<List<PaymentOrderDTO>> generateFromPayrollRun(
|
||||
@Valid @RequestBody GenerateOrdersFromPayrollRunDTO dto) {
|
||||
try {
|
||||
List<PaymentOrderDTO> orders = paymentOrderService.generateOrdersInternal(
|
||||
dto.getPayrollRunId(),
|
||||
dto.getPaymentBatchId());
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(orders);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (RuntimeException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CreateTreasuryEntryDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.TreasuryEntryDTO;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
import br.gov.sigefp.treasury.service.TreasuryEntryService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/entries")
|
||||
@RequiredArgsConstructor
|
||||
public class TreasuryEntryController {
|
||||
|
||||
private final TreasuryEntryService treasuryEntryService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<TreasuryEntryDTO> create(@Valid @RequestBody CreateTreasuryEntryDTO dto) {
|
||||
TreasuryEntryDTO created = treasuryEntryService.create(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<TreasuryEntryDTO> findById(@PathVariable UUID id) {
|
||||
TreasuryEntryDTO entry = treasuryEntryService.findById(id);
|
||||
return ResponseEntity.ok(entry);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<TreasuryEntryDTO>> findAll(
|
||||
@RequestParam(required = false) UUID cashAccountId,
|
||||
@RequestParam(required = false) TreasuryEntryType type,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(defaultValue = "transactionDate") String sortBy,
|
||||
@RequestParam(defaultValue = "DESC") String sortDirection) {
|
||||
|
||||
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
Page<TreasuryEntryDTO> result;
|
||||
if (cashAccountId != null) {
|
||||
result = treasuryEntryService.findByCashAccountId(cashAccountId, pageable);
|
||||
} else if (type != null) {
|
||||
result = treasuryEntryService.findByType(type, pageable);
|
||||
} else if (status != null) {
|
||||
result = treasuryEntryService.findByStatus(status, pageable);
|
||||
} else {
|
||||
// Retornar vazio se nenhum filtro for fornecido
|
||||
result = Page.empty(pageable);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{cashAccountId}/available-balance")
|
||||
public ResponseEntity<BigDecimal> getAvailableBalance(@PathVariable UUID cashAccountId) {
|
||||
BigDecimal balance = treasuryEntryService.calculateAvailableBalance(cashAccountId);
|
||||
return ResponseEntity.ok(balance);
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CreateTreasuryPaymentDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.TreasuryPaymentDTO;
|
||||
import br.gov.sigefp.treasury.service.TreasuryPaymentService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para gestão de pagamentos efetivados pela tesouraria.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/payments")
|
||||
@RequiredArgsConstructor
|
||||
public class TreasuryPaymentController {
|
||||
|
||||
private final TreasuryPaymentService treasuryPaymentService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(name = "paymentOrderId", required = false) UUID paymentOrderId,
|
||||
@RequestParam(name = "status", required = false) String status,
|
||||
@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) {
|
||||
|
||||
// Se há filtros, retornar lista (sem paginação)
|
||||
if (paymentOrderId != null || status != null) {
|
||||
List<TreasuryPaymentDTO> result;
|
||||
if (paymentOrderId != null && status != null) {
|
||||
result = treasuryPaymentService.findByPaymentOrderIdAndStatus(paymentOrderId, status);
|
||||
} else if (paymentOrderId != null) {
|
||||
result = treasuryPaymentService.findByPaymentOrderId(paymentOrderId);
|
||||
} else {
|
||||
result = treasuryPaymentService.findByStatus(status);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
// Sem filtros, retornar paginado
|
||||
Sort sort = sortBy != null
|
||||
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
|
||||
: Sort.by(Sort.Direction.DESC, "paidAt");
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
Page<TreasuryPaymentDTO> result = treasuryPaymentService.findAll(pageable);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<TreasuryPaymentDTO> registerPayment(@Valid @RequestBody CreateTreasuryPaymentDTO dto) {
|
||||
try {
|
||||
TreasuryPaymentDTO created = treasuryPaymentService.registerPayment(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package br.gov.sigefp.treasury.api;
|
||||
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateTreasuryPlanDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.TreasuryPlanDTO;
|
||||
import br.gov.sigefp.treasury.service.TreasuryPlanService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller REST para gestão de Planos de Tesouraria.
|
||||
* Conforme Master Plan - Module 2: Preventive Control.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/treasury/plans")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TreasuryPlanController {
|
||||
|
||||
private final TreasuryPlanService treasuryPlanService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<TreasuryPlanDTO> create(@Valid @RequestBody CreateTreasuryPlanDTO dto) {
|
||||
TreasuryPlanDTO created = treasuryPlanService.createPlan(dto);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(created);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<TreasuryPlanDTO> findById(@PathVariable UUID id) {
|
||||
try {
|
||||
TreasuryPlanDTO plan = treasuryPlanService.findById(id);
|
||||
return ResponseEntity.ok(plan);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
log.warn("Plano não encontrado: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/status/{status}")
|
||||
public ResponseEntity<List<TreasuryPlanDTO>> findByStatus(@PathVariable String status) {
|
||||
List<TreasuryPlanDTO> plans = treasuryPlanService.findByStatus(status);
|
||||
return ResponseEntity.ok(plans);
|
||||
}
|
||||
|
||||
@GetMapping("/active")
|
||||
public ResponseEntity<TreasuryPlanDTO> findActivePlan(
|
||||
@RequestParam(required = false) String date) {
|
||||
try {
|
||||
LocalDate searchDate;
|
||||
if (date != null) {
|
||||
try {
|
||||
searchDate = LocalDate.parse(date);
|
||||
} catch (DateTimeParseException e) {
|
||||
log.warn("Data inválida fornecida: {}", date);
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
} else {
|
||||
searchDate = LocalDate.now();
|
||||
}
|
||||
|
||||
TreasuryPlanDTO plan = treasuryPlanService.findActivePlanForDate(searchDate);
|
||||
if (plan == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(plan);
|
||||
} catch (Exception e) {
|
||||
log.error("Erro ao buscar plano ativo", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/approve")
|
||||
public ResponseEntity<TreasuryPlanDTO> approve(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam UUID approverId) {
|
||||
try {
|
||||
TreasuryPlanDTO approved = treasuryPlanService.approvePlan(id, approverId);
|
||||
return ResponseEntity.ok(approved);
|
||||
} catch (ResourceNotFoundException e) {
|
||||
log.warn("Plano não encontrado para aprovação: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erro ao aprovar plano: {}", id, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de aprovação individual.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApprovalDTO {
|
||||
|
||||
private UUID id;
|
||||
private UUID authorizationId;
|
||||
private Integer level;
|
||||
private UUID approvedBy;
|
||||
private LocalDateTime approvedAt;
|
||||
private String comments;
|
||||
private String signatureHash;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para aprovação de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ApprovePaymentDTO {
|
||||
|
||||
@NotNull(message = "ID do aprovador é obrigatório")
|
||||
private UUID approverId;
|
||||
|
||||
private String comments;
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de conciliação bancária.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BankReconciliationDTO {
|
||||
|
||||
private UUID id;
|
||||
private UUID cashAccountId;
|
||||
private LocalDate reconciliationDate;
|
||||
private BigDecimal statementBalance;
|
||||
private BigDecimal systemBalance;
|
||||
private BigDecimal difference;
|
||||
private String status; // PENDING, RECONCILED, DISCREPANCY
|
||||
private UUID reconciledBy;
|
||||
private LocalDateTime reconciledAt;
|
||||
private List<ReconciliationItemDTO> items;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
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 conta de caixa/bancária.
|
||||
* Inclui novos campos conforme Master Plan: parentId, category, iban, swiftCode.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CashAccountDTO {
|
||||
|
||||
private UUID id;
|
||||
private String code;
|
||||
private String name;
|
||||
private String type; // CASH, BANK_ACCOUNT
|
||||
private UUID bankId;
|
||||
private String accountNumber;
|
||||
private String branchCode;
|
||||
|
||||
// Novos campos conforme Master Plan
|
||||
private String iban; // IBAN da conta (ISO format)
|
||||
private String swiftCode; // Código SWIFT
|
||||
private UUID parentId; // Hierarquia CUT - Conta pai
|
||||
private String category; // CENTRAL_CUT, SUB_ACCOUNT, TRANSIT, REVENUE
|
||||
private BigDecimal overdraftLimit; // Limite de descoberto autorizado
|
||||
|
||||
private String currency;
|
||||
private Boolean isActive;
|
||||
private BigDecimal currentBalance;
|
||||
private BigDecimal availableBalance;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de fluxo de caixa.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CashFlowDTO {
|
||||
|
||||
private UUID id;
|
||||
private UUID cashAccountId;
|
||||
private LocalDate transactionDate;
|
||||
private String type; // INFLOW, OUTFLOW
|
||||
private BigDecimal amount;
|
||||
private String description;
|
||||
private UUID referenceId;
|
||||
private String referenceType; // PAYMENT_ORDER, TREASURY_ENTRY, etc.
|
||||
private BigDecimal balanceAfter;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de conciliação bancária.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateBankReconciliationDTO {
|
||||
|
||||
@NotNull(message = "Conta bancária é obrigatória")
|
||||
private UUID cashAccountId;
|
||||
|
||||
@NotNull(message = "Data de conciliação é obrigatória")
|
||||
private LocalDate reconciliationDate;
|
||||
|
||||
@NotNull(message = "Saldo do extrato é obrigatório")
|
||||
private BigDecimal statementBalance;
|
||||
|
||||
@NotEmpty(message = "Itens de conciliação são obrigatórios")
|
||||
private List<ReconciliationItemDTO> reconciliationItems;
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package br.gov.sigefp.treasury.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 criação de conta de caixa/bancária.
|
||||
* Inclui novos campos conforme Master Plan: parentId, category, iban, swiftCode.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateCashAccountDTO {
|
||||
|
||||
@NotBlank(message = "Código é obrigatório")
|
||||
@Size(max = 50, message = "Código deve ter no máximo 50 caracteres")
|
||||
private String code;
|
||||
|
||||
@NotBlank(message = "Nome é obrigatório")
|
||||
@Size(max = 200, message = "Nome deve ter no máximo 200 caracteres")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "Tipo é obrigatório")
|
||||
private String type; // CASH, BANK_ACCOUNT
|
||||
|
||||
@NotNull(message = "Unidade Orgânica é obrigatória")
|
||||
private UUID orgUnitId;
|
||||
|
||||
private UUID bankId; // Obrigatório se type = BANK_ACCOUNT
|
||||
|
||||
@Size(max = 50, message = "Número da conta deve ter no máximo 50 caracteres")
|
||||
private String accountNumber;
|
||||
|
||||
@Size(max = 20, message = "Código da agência deve ter no máximo 20 caracteres")
|
||||
private String branchCode;
|
||||
|
||||
// Novos campos conforme Master Plan
|
||||
@Size(max = 34, message = "IBAN deve ter no máximo 34 caracteres")
|
||||
private String iban; // IBAN da conta (ISO format)
|
||||
|
||||
@Size(max = 11, message = "Código SWIFT deve ter no máximo 11 caracteres")
|
||||
private String swiftCode; // Código SWIFT
|
||||
|
||||
private UUID parentId; // Hierarquia CUT - Conta pai (self-reference)
|
||||
|
||||
private String category; // CENTRAL_CUT, SUB_ACCOUNT, TRANSIT, REVENUE
|
||||
|
||||
private BigDecimal overdraftLimit; // Limite de descoberto autorizado
|
||||
|
||||
@Size(max = 3, message = "Moeda deve ter no máximo 3 caracteres")
|
||||
private String currency; // Default: XOF
|
||||
|
||||
private Boolean isActive;
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de fluxo de caixa.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateCashFlowDTO {
|
||||
|
||||
@NotNull(message = "Conta de caixa é obrigatória")
|
||||
private UUID cashAccountId;
|
||||
|
||||
@NotNull(message = "Data da transação é obrigatória")
|
||||
private LocalDate transactionDate;
|
||||
|
||||
@NotNull(message = "Tipo é obrigatório")
|
||||
private String type; // INFLOW, OUTFLOW
|
||||
|
||||
@NotNull(message = "Valor é obrigatório")
|
||||
@Positive(message = "Valor deve ser positivo")
|
||||
private BigDecimal amount;
|
||||
|
||||
private String description;
|
||||
|
||||
private UUID referenceId;
|
||||
|
||||
private String referenceType;
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de autorização de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreatePaymentAuthorizationDTO {
|
||||
|
||||
private UUID paymentOrderId; // Um dos dois deve ser fornecido
|
||||
private UUID paymentBatchId; // Um dos dois deve ser fornecido
|
||||
|
||||
@NotNull(message = "Usuário solicitante é obrigatório")
|
||||
private UUID requestedBy;
|
||||
|
||||
@NotNull(message = "Nível de aprovação necessário é obrigatório")
|
||||
@Min(value = 1, message = "Nível de aprovação deve ser pelo menos 1")
|
||||
private Integer requiredApprovalLevel;
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de lote de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreatePaymentBatchDTO {
|
||||
|
||||
@NotNull(message = "ID do período é obrigatório")
|
||||
private Long periodId;
|
||||
|
||||
@NotNull(message = "ID do ministério é obrigatório")
|
||||
private UUID ministryId;
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de ordem de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreatePaymentOrderDTO {
|
||||
|
||||
@NotNull(message = "ID do lote de pagamento é obrigatório")
|
||||
private UUID paymentBatchId;
|
||||
|
||||
private UUID payrollRunId;
|
||||
|
||||
@NotNull(message = "ID do agente é obrigatório")
|
||||
private UUID agentId;
|
||||
|
||||
@NotNull(message = "ID da conta bancária é obrigatório")
|
||||
private UUID bankAccountId;
|
||||
|
||||
@NotNull(message = "Valor bruto é obrigatório")
|
||||
@Positive(message = "Valor bruto deve ser positivo")
|
||||
private BigDecimal grossAmount;
|
||||
|
||||
@NotNull(message = "Valor líquido é obrigatório")
|
||||
@Positive(message = "Valor líquido deve ser positivo")
|
||||
private BigDecimal netAmount;
|
||||
|
||||
private BigDecimal taxAmount;
|
||||
private String taxRetentionType;
|
||||
private UUID taxCollectionAccountId;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de entrada de tesouraria.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateTreasuryEntryDTO {
|
||||
|
||||
@NotNull(message = "Tipo é obrigatório")
|
||||
private TreasuryEntryType type;
|
||||
|
||||
@NotNull(message = "Valor é obrigatório")
|
||||
@Positive(message = "Valor deve ser positivo")
|
||||
private BigDecimal amount;
|
||||
|
||||
@NotNull(message = "Data da transação é obrigatória")
|
||||
private LocalDate transactionDate;
|
||||
|
||||
@NotNull(message = "Referência do documento é obrigatória")
|
||||
private String documentReference;
|
||||
|
||||
private String description;
|
||||
|
||||
private String status; // DRAFT, PENDING_APPROVAL
|
||||
|
||||
private Integer approvalLevel;
|
||||
|
||||
private UUID cashAccountId;
|
||||
|
||||
private UUID paymentOrderId;
|
||||
|
||||
private UUID paymentBatchId;
|
||||
|
||||
private UUID budgetLineId;
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para criação de pagamento efetivado.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateTreasuryPaymentDTO {
|
||||
|
||||
@NotNull(message = "ID da ordem de pagamento é obrigatório")
|
||||
private UUID paymentOrderId;
|
||||
|
||||
private Instant paidAt;
|
||||
|
||||
@Size(max = 100, message = "Referência da transação deve ter no máximo 100 caracteres")
|
||||
private String transactionRef;
|
||||
|
||||
@NotBlank(message = "Status é obrigatório")
|
||||
private String status; // PENDING, PAID, REJECTED, CANCELLED
|
||||
|
||||
@Size(max = 1000, message = "Mensagem deve ter no máximo 1000 caracteres")
|
||||
private String message;
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* DTO para criação de Plano de Tesouraria.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateTreasuryPlanDTO {
|
||||
|
||||
@NotNull(message = "Ano fiscal é obrigatório")
|
||||
@Min(value = 2020, message = "Ano fiscal deve ser a partir de 2020")
|
||||
private Integer fiscalYear;
|
||||
|
||||
@NotNull(message = "Mês de referência é obrigatório")
|
||||
@Min(value = 1, message = "Mês deve ser entre 1 e 12")
|
||||
@Max(value = 12, message = "Mês deve ser entre 1 e 12")
|
||||
private Integer referenceMonth;
|
||||
|
||||
@NotNull(message = "Teto aprovado é obrigatório")
|
||||
@Positive(message = "Teto aprovado deve ser positivo")
|
||||
private BigDecimal approvedCeiling;
|
||||
|
||||
private LocalDate startDate; // Opcional - calculado automaticamente se não fornecido
|
||||
private LocalDate endDate; // Opcional - calculado automaticamente se não fornecido
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para gerar ordens de pagamento a partir de uma execução de folha.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GenerateOrdersFromPayrollRunDTO {
|
||||
|
||||
@NotNull(message = "ID da execução de folha é obrigatório")
|
||||
private UUID payrollRunId;
|
||||
|
||||
@NotNull(message = "ID do lote de pagamento é obrigatório")
|
||||
private UUID paymentBatchId;
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para parear item de conciliação com transação do sistema.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MatchReconciliationItemDTO {
|
||||
|
||||
@NotNull(message = "ID da transação do sistema é obrigatório")
|
||||
private UUID systemTransactionId;
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de autorização de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentAuthorizationDTO {
|
||||
|
||||
private UUID id;
|
||||
private UUID paymentOrderId;
|
||||
private UUID paymentBatchId;
|
||||
private UUID requestedBy;
|
||||
private LocalDateTime requestedAt;
|
||||
private Integer requiredApprovalLevel;
|
||||
private Integer currentApprovalLevel;
|
||||
private String status; // PENDING, PARTIALLY_APPROVED, APPROVED, REJECTED
|
||||
private String rejectionReason;
|
||||
private List<ApprovalDTO> approvals;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de lote de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentBatchDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotNull(message = "ID do período é obrigatório")
|
||||
private Long periodId;
|
||||
|
||||
@NotNull(message = "ID do ministério é obrigatório")
|
||||
private UUID ministryId;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private UUID createdBy;
|
||||
|
||||
@NotBlank(message = "Status é obrigatório")
|
||||
private String status; // CREATED, SENT_TO_BANK, CONFIRMED, REJECTED
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
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 ordem de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentOrderDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
private UUID paymentBatchId;
|
||||
|
||||
private UUID payrollRunId;
|
||||
|
||||
private UUID agentId;
|
||||
|
||||
private UUID bankAccountId;
|
||||
|
||||
private UUID budgetLineId; // Referência à linha orçamentária
|
||||
|
||||
@NotNull(message = "Valor bruto é obrigatório")
|
||||
@Positive(message = "Valor bruto deve ser positivo")
|
||||
private BigDecimal grossAmount;
|
||||
|
||||
@NotNull(message = "Valor líquido é obrigatório")
|
||||
@Positive(message = "Valor líquido deve ser positivo")
|
||||
private BigDecimal netAmount;
|
||||
|
||||
private BigDecimal taxAmount;
|
||||
private String taxRetentionType;
|
||||
private UUID taxCollectionAccountId;
|
||||
|
||||
private String status; // CREATED, SENT_TO_BANK, PAID, REJECTED
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de item de conciliação.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReconciliationItemDTO {
|
||||
|
||||
private UUID id;
|
||||
private UUID reconciliationId;
|
||||
private LocalDate transactionDate;
|
||||
private String description;
|
||||
private BigDecimal statementAmount;
|
||||
private BigDecimal systemAmount;
|
||||
private String matchStatus; // MATCHED, UNMATCHED, PENDING
|
||||
private UUID matchedTransactionId;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para rejeição de pagamento.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RejectPaymentDTO {
|
||||
|
||||
@NotNull(message = "ID do aprovador é obrigatório")
|
||||
private UUID approverId;
|
||||
|
||||
@NotBlank(message = "Motivo da rejeição é obrigatório")
|
||||
private String reason;
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de entrada de tesouraria.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TreasuryEntryDTO {
|
||||
|
||||
private UUID id;
|
||||
private TreasuryEntryType type;
|
||||
private BigDecimal amount;
|
||||
private LocalDate transactionDate;
|
||||
private String documentReference;
|
||||
private String description;
|
||||
private String status; // DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, EXECUTED
|
||||
private Integer approvalLevel;
|
||||
private UUID approvedBy;
|
||||
private LocalDateTime approvedAt;
|
||||
private UUID cashAccountId;
|
||||
private UUID paymentOrderId;
|
||||
private UUID paymentBatchId;
|
||||
private UUID budgetLineId;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de pagamento efetivado pela tesouraria.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TreasuryPaymentDTO {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotNull(message = "ID da ordem de pagamento é obrigatório")
|
||||
private UUID paymentOrderId;
|
||||
|
||||
private Instant paidAt;
|
||||
|
||||
@Size(max = 100, message = "Referência da transação deve ter no máximo 100 caracteres")
|
||||
private String transactionRef;
|
||||
|
||||
@NotBlank(message = "Status é obrigatório")
|
||||
private String status; // PENDING, PAID, REJECTED, CANCELLED
|
||||
|
||||
@Size(max = 1000, message = "Mensagem deve ter no máximo 1000 caracteres")
|
||||
private String message;
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO para transferência de dados de Plano de Tesouraria.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TreasuryPlanDTO {
|
||||
|
||||
private UUID id;
|
||||
private Integer fiscalYear;
|
||||
private Integer referenceMonth; // 1-12
|
||||
private String status; // DRAFT, APPROVED, CLOSED
|
||||
private BigDecimal approvedCeiling;
|
||||
private BigDecimal executedAmount; // Calculado dinamicamente
|
||||
private BigDecimal availableAmount; // approvedCeiling - executedAmount
|
||||
private LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
private UUID approvedBy;
|
||||
private LocalDateTime approvedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
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 atualização de conta de caixa/bancária.
|
||||
* Inclui novos campos conforme Master Plan: parentId, category, iban, swiftCode.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UpdateCashAccountDTO {
|
||||
|
||||
private String name;
|
||||
|
||||
@Size(max = 34, message = "IBAN deve ter no máximo 34 caracteres")
|
||||
private String iban;
|
||||
|
||||
@Size(max = 11, message = "Código SWIFT deve ter no máximo 11 caracteres")
|
||||
private String swiftCode;
|
||||
|
||||
private UUID parentId; // Hierarquia CUT - Conta pai
|
||||
|
||||
private String category; // CENTRAL_CUT, SUB_ACCOUNT, TRANSIT, REVENUE
|
||||
|
||||
private Boolean isActive;
|
||||
|
||||
private BigDecimal overdraftLimit; // Limite de descoberto autorizado
|
||||
}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package br.gov.sigefp.treasury.api.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* DTO para atualização de status.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UpdateStatusDTO {
|
||||
|
||||
@NotBlank(message = "Status é obrigatório")
|
||||
private String status;
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma aprovação individual em um workflow hierárquico.
|
||||
* Parte do histórico de aprovações de PaymentAuthorization.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "approval", indexes = {
|
||||
@Index(name = "idx_approval_authorization", columnList = "authorization_id"),
|
||||
@Index(name = "idx_approval_approver", columnList = "approved_by"),
|
||||
@Index(name = "idx_approval_level", columnList = "level")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Approval {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "authorization_id", nullable = false)
|
||||
private PaymentAuthorization authorization;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer level; // Nível da aprovação (1, 2, 3...)
|
||||
|
||||
@Column(name = "approved_by", nullable = false)
|
||||
private UUID approvedBy; // Usuário que aprovou
|
||||
|
||||
@Column(name = "approved_at", nullable = false)
|
||||
private LocalDateTime approvedAt;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String comments;
|
||||
|
||||
@Column(name = "signature_hash", length = 255)
|
||||
private String signatureHash; // Hash da assinatura digital (futuro)
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package br.gov.sigefp.treasury.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;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma conciliação bancária.
|
||||
* Compara saldos e transações do sistema com extratos bancários.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "bank_reconciliation", indexes = {
|
||||
@Index(name = "idx_reconciliation_cash_account", columnList = "cash_account_id"),
|
||||
@Index(name = "idx_reconciliation_date", columnList = "reconciliation_date"),
|
||||
@Index(name = "idx_reconciliation_status", columnList = "status")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class BankReconciliation extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cash_account_id", nullable = false)
|
||||
private CashAccount cashAccount;
|
||||
|
||||
@Column(name = "reconciliation_date", nullable = false)
|
||||
private LocalDate reconciliationDate;
|
||||
|
||||
@Column(name = "statement_balance", nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal statementBalance; // Saldo do extrato bancário
|
||||
|
||||
@Column(name = "system_balance", nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal systemBalance; // Saldo do sistema
|
||||
|
||||
@Column(precision = 19, scale = 2)
|
||||
private BigDecimal difference; // Diferença (statementBalance - systemBalance)
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "PENDING"; // PENDING, RECONCILED, DISCREPANCY
|
||||
|
||||
@Column(name = "reconciled_by")
|
||||
private UUID reconciledBy; // Usuário que conciliou
|
||||
|
||||
@Column(name = "reconciled_at")
|
||||
private LocalDateTime reconciledAt;
|
||||
|
||||
@OneToMany(mappedBy = "reconciliation", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<ReconciliationItem> reconciliationItems = new ArrayList<>();
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma conta de caixa ou bancária do Tesouro.
|
||||
* Gerencia disponibilidades e saldos.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "cash_account", indexes = {
|
||||
@Index(name = "idx_cash_account_code", columnList = "code"),
|
||||
@Index(name = "idx_cash_account_type", columnList = "type"),
|
||||
@Index(name = "idx_cash_account_active", columnList = "is_active")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class CashAccount extends BaseEntity {
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String code; // Ex: "CAIXA-001", "BANCO-BCEAO-001"
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String name; // Ex: "Caixa Principal", "Conta BCEAO - Tesouro"
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String type; // CASH, BANK_ACCOUNT
|
||||
|
||||
@Column(name = "bank_id")
|
||||
private java.util.UUID bankId; // Se tipo = BANK_ACCOUNT
|
||||
|
||||
@Column(name = "parent_id")
|
||||
private java.util.UUID parentId; // Conta Pai (Hierarquia CUT)
|
||||
|
||||
@Column(name = "category", length = 20)
|
||||
private String category; // TRANSIT, CENTRAL_CUT, SUB_ACCOUNT
|
||||
|
||||
@Column(name = "account_number", length = 50)
|
||||
private String accountNumber;
|
||||
|
||||
@Column(name = "branch_code", length = 20)
|
||||
private String branchCode;
|
||||
|
||||
@Column(nullable = false, length = 3)
|
||||
@Builder.Default
|
||||
private String currency = "XOF";
|
||||
|
||||
@Column(name = "org_unit_id", nullable = true)
|
||||
private java.util.UUID orgUnitId; // Vínculo com Unidade Orgânica (Ministério/Direção)
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean isActive = true;
|
||||
|
||||
@Column(name = "current_balance", nullable = false, precision = 19, scale = 2)
|
||||
@Builder.Default
|
||||
private BigDecimal currentBalance = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "available_balance", nullable = false, precision = 19, scale = 2)
|
||||
@Builder.Default
|
||||
private BigDecimal availableBalance = BigDecimal.ZERO; // Saldo disponível (após compromissos)
|
||||
|
||||
@OneToMany(mappedBy = "cashAccount", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<TreasuryEntry> treasuryEntries = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "cashAccount", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<CashFlow> cashFlows = new ArrayList<>();
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package br.gov.sigefp.treasury.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;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma transação de fluxo de caixa.
|
||||
* Rastreia entradas e saídas de caixa.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "cash_flow", indexes = {
|
||||
@Index(name = "idx_cash_flow_account", columnList = "cash_account_id"),
|
||||
@Index(name = "idx_cash_flow_date", columnList = "transaction_date"),
|
||||
@Index(name = "idx_cash_flow_type", columnList = "type"),
|
||||
@Index(name = "idx_cash_flow_reference", columnList = "reference_type,reference_id")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class CashFlow extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cash_account_id", nullable = false)
|
||||
private CashAccount cashAccount;
|
||||
|
||||
@Column(name = "transaction_date", nullable = false)
|
||||
private LocalDate transactionDate;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String type; // INFLOW, OUTFLOW
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "reference_id")
|
||||
private UUID referenceId; // ID da entidade relacionada
|
||||
|
||||
@Column(name = "reference_type", length = 50)
|
||||
private String referenceType; // PAYMENT_ORDER, TREASURY_ENTRY, etc.
|
||||
|
||||
@Column(name = "balance_after", nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal balanceAfter; // Saldo após a transação
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.AuditableEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa um pagamento.
|
||||
* Usa LocalDate para todas as datas relacionadas.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "payments", indexes = {
|
||||
@Index(name = "idx_payment_number", columnList = "payment_number"),
|
||||
@Index(name = "idx_payment_date", columnList = "payment_date"),
|
||||
@Index(name = "idx_payment_status", columnList = "status")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Payment extends AuditableEntity {
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String paymentNumber;
|
||||
|
||||
@Column(name = "budget_execution_id")
|
||||
private UUID budgetExecutionId; // Referência à execução orçamentária
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate paymentDate;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate dueDate;
|
||||
|
||||
private LocalDate paidDate;
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
@Builder.Default
|
||||
private String status = "PENDING"; // PENDING, APPROVED, PAID, CANCELLED
|
||||
|
||||
@Column(nullable = false, length = 200)
|
||||
private String beneficiary;
|
||||
|
||||
@Column(length = 50)
|
||||
private String beneficiaryDocument; // NIF, CNPJ, etc.
|
||||
|
||||
@Column(length = 500)
|
||||
private String description;
|
||||
|
||||
@Column(length = 100)
|
||||
private String paymentMethod; // BANK_TRANSFER, CHECK, CASH, etc.
|
||||
|
||||
@Column(length = 500)
|
||||
private String notes;
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa um workflow de aprovação hierárquica de pagamentos.
|
||||
* Gerencia o processo de autorização de pagamentos por níveis.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "payment_authorization", indexes = {
|
||||
@Index(name = "idx_auth_payment_order", columnList = "payment_order_id"),
|
||||
@Index(name = "idx_auth_payment_batch", columnList = "payment_batch_id"),
|
||||
@Index(name = "idx_auth_status", columnList = "status"),
|
||||
@Index(name = "idx_auth_requested_by", columnList = "requested_by")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class PaymentAuthorization extends BaseEntity {
|
||||
|
||||
@Column(name = "payment_order_id")
|
||||
private UUID paymentOrderId; // Ordem de pagamento individual
|
||||
|
||||
@Column(name = "payment_batch_id")
|
||||
private UUID paymentBatchId; // Lote de pagamento
|
||||
|
||||
@Column(name = "requested_by", nullable = false)
|
||||
private UUID requestedBy; // Usuário que solicitou
|
||||
|
||||
@Column(name = "requested_at", nullable = false)
|
||||
private LocalDateTime requestedAt;
|
||||
|
||||
@Column(name = "required_approval_level", nullable = false)
|
||||
private Integer requiredApprovalLevel; // Nível necessário (1, 2, 3...)
|
||||
|
||||
@Column(name = "current_approval_level", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer currentApprovalLevel = 1; // Nível atual
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "PENDING"; // PENDING, PARTIALLY_APPROVED, APPROVED, REJECTED
|
||||
|
||||
@Column(name = "rejection_reason", columnDefinition = "TEXT")
|
||||
private String rejectionReason;
|
||||
|
||||
@OneToMany(mappedBy = "authorization", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<Approval> approvals = new ArrayList<>(); // Histórico de aprovações
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa um lote de pagamentos.
|
||||
* Usa IDs para referências a outros módulos.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "payment_batch", indexes = {
|
||||
@Index(name = "idx_payment_batch_period", columnList = "period_id"),
|
||||
@Index(name = "idx_payment_batch_ministry", columnList = "ministry_id"),
|
||||
@Index(name = "idx_payment_batch_status", columnList = "status"),
|
||||
@Index(name = "idx_payment_batch_created", columnList = "created_at")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class PaymentBatch extends BaseEntity {
|
||||
|
||||
@Column(name = "period_id")
|
||||
private Long periodId; // Referência ao período de folha (rh.payroll_period)
|
||||
|
||||
@Column(name = "ministry_id")
|
||||
private UUID ministryId; // Referência ao ministério (org.ministry)
|
||||
|
||||
@Column(name = "created_by")
|
||||
private UUID createdBy; // ID do usuário que criou o lote
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "CREATED"; // CREATED, PROCESSING, SENT_TO_BANK, COMPLETED, REJECTED - preparado para enum
|
||||
|
||||
@Column(length = 255)
|
||||
private String reference;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@OneToMany(mappedBy = "paymentBatch", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<PaymentOrder> paymentOrders = new ArrayList<>();
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma ordem de pagamento.
|
||||
* Usa BigDecimal para valores financeiros e IDs para referências a outros
|
||||
* módulos.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "payment_order", indexes = {
|
||||
@Index(name = "idx_payment_order_batch", columnList = "payment_batch_id"),
|
||||
@Index(name = "idx_payment_order_payroll_run", columnList = "payroll_run_id"),
|
||||
@Index(name = "idx_payment_order_agent", columnList = "agent_id"),
|
||||
@Index(name = "idx_payment_order_status", columnList = "status")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class PaymentOrder extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "payment_batch_id", nullable = false)
|
||||
private PaymentBatch paymentBatch;
|
||||
|
||||
@Column(name = "payroll_run_id")
|
||||
private UUID payrollRunId; // Referência à execução de folha (rh.payroll_run)
|
||||
|
||||
@Column(name = "agent_id")
|
||||
private UUID agentId; // Referência ao agente (rh.agent)
|
||||
|
||||
@Column(name = "bank_account_id")
|
||||
private UUID bankAccountId; // Referência à conta bancária (rh.agent_bank_account)
|
||||
|
||||
@Column(name = "budget_line_id")
|
||||
private UUID budgetLineId; // Referência à linha orçamentária (para execução orçamentária)
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2, name = "gross_amount")
|
||||
private BigDecimal grossAmount; // Valor bruto
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2, name = "net_amount")
|
||||
private BigDecimal netAmount; // Valor líquido (após descontos)
|
||||
|
||||
// --- Campos de Integridade Fiscal (RN03) ---
|
||||
@Column(nullable = false, precision = 19, scale = 2, name = "tax_amount")
|
||||
@Builder.Default
|
||||
private BigDecimal taxAmount = BigDecimal.ZERO; // Valor do Imposto Retido
|
||||
|
||||
@Column(name = "tax_retention_type", length = 20)
|
||||
private String taxRetentionType; // Ex: IR_20, IVA_18, NONE
|
||||
|
||||
@Column(name = "tax_collection_account_id")
|
||||
private UUID taxCollectionAccountId; // Conta de Arrecadação (DGI) para onde vai o imposto
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "CREATED"; // CREATED, SENT_TO_BANK, PAID, REJECTED - preparado para enum
|
||||
|
||||
@OneToMany(mappedBy = "paymentOrder", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<TreasuryPayment> treasuryPayments = new ArrayList<>();
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa um item de conciliação bancária.
|
||||
* Parte de BankReconciliation.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "reconciliation_item", indexes = {
|
||||
@Index(name = "idx_reconciliation_item_reconciliation", columnList = "reconciliation_id"),
|
||||
@Index(name = "idx_reconciliation_item_status", columnList = "match_status"),
|
||||
@Index(name = "idx_reconciliation_item_date", columnList = "transaction_date")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ReconciliationItem {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "reconciliation_id", nullable = false)
|
||||
private BankReconciliation reconciliation;
|
||||
|
||||
@Column(name = "transaction_date", nullable = false)
|
||||
private LocalDate transactionDate;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "statement_amount", precision = 19, scale = 2)
|
||||
private BigDecimal statementAmount; // Valor do extrato bancário
|
||||
|
||||
@Column(name = "system_amount", precision = 19, scale = 2)
|
||||
private BigDecimal systemAmount; // Valor do sistema
|
||||
|
||||
@Column(name = "match_status", nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String matchStatus = "PENDING"; // MATCHED, UNMATCHED, PENDING
|
||||
|
||||
@Column(name = "matched_transaction_id")
|
||||
private UUID matchedTransactionId; // ID da transação do sistema que corresponde
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package br.gov.sigefp.treasury.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;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entidade que representa uma entrada de tesouraria.
|
||||
* Similar a BudgetEntry no módulo de Orçamento.
|
||||
* Rastreia todas as movimentações de tesouraria com histórico completo.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "treasury_entry", indexes = {
|
||||
@Index(name = "idx_treasury_entry_cash_account", columnList = "cash_account_id"),
|
||||
@Index(name = "idx_treasury_entry_type", columnList = "type"),
|
||||
@Index(name = "idx_treasury_entry_status", columnList = "status"),
|
||||
@Index(name = "idx_treasury_entry_date", columnList = "transaction_date"),
|
||||
@Index(name = "idx_treasury_entry_payment_order", columnList = "payment_order_id")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class TreasuryEntry extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cash_account_id", nullable = false)
|
||||
private CashAccount cashAccount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 50)
|
||||
private TreasuryEntryType type;
|
||||
|
||||
@Column(nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "transaction_date", nullable = false)
|
||||
private LocalDate transactionDate; // Data da transação
|
||||
|
||||
@Column(name = "document_reference", length = 100)
|
||||
private String documentReference; // Ex: "Decreto de Autorização", "Portaria de Pagamento"
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT"; // DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, EXECUTED
|
||||
|
||||
@Column(name = "approval_level")
|
||||
private Integer approvalLevel; // Nível de aprovação necessário
|
||||
|
||||
@Column(name = "approved_by")
|
||||
private UUID approvedBy; // Usuário que aprovou
|
||||
|
||||
@Column(name = "approved_at")
|
||||
private LocalDateTime approvedAt;
|
||||
|
||||
@Column(name = "payment_order_id")
|
||||
private UUID paymentOrderId; // Referência à ordem de pagamento (se aplicável)
|
||||
|
||||
@Column(name = "payment_batch_id")
|
||||
private UUID paymentBatchId; // Referência ao lote de pagamento (se aplicável)
|
||||
|
||||
@Column(name = "budget_line_id")
|
||||
private UUID budgetLineId; // Linha orçamentária relacionada
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
/**
|
||||
* Tipos de entradas de tesouraria.
|
||||
* Similar a BudgetEntryType no módulo de Orçamento.
|
||||
*/
|
||||
public enum TreasuryEntryType {
|
||||
// Autorizações
|
||||
PAYMENT_AUTHORIZATION, // Autorização de Pagamento
|
||||
BATCH_AUTHORIZATION, // Autorização de Lote
|
||||
|
||||
// Disponibilidades
|
||||
CASH_AVAILABILITY, // Disponibilidade de Caixa
|
||||
BANK_AVAILABILITY, // Disponibilidade Bancária
|
||||
CASH_DEPOSIT, // Depósito em Caixa
|
||||
CASH_WITHDRAWAL, // Saque de Caixa
|
||||
|
||||
// Programação
|
||||
PAYMENT_SCHEDULING, // Programação de Pagamento
|
||||
BATCH_SCHEDULING, // Programação de Lote
|
||||
|
||||
// Execução
|
||||
PAYMENT_EXECUTION, // Execução de Pagamento
|
||||
BATCH_EXECUTION, // Execução de Lote
|
||||
|
||||
// Conciliação
|
||||
BANK_RECONCILIATION, // Conciliação Bancária
|
||||
CASH_RECONCILIATION, // Conciliação de Caixa
|
||||
|
||||
// Ajustes
|
||||
PAYMENT_CANCELLATION, // Cancelamento de Pagamento
|
||||
PAYMENT_ADJUSTMENT, // Ajuste de Pagamento
|
||||
CASH_ADJUSTMENT // Ajuste de Caixa
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package br.gov.sigefp.treasury.domain;
|
||||
|
||||
import br.gov.sigefp.common.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Entidade que representa um pagamento efetivado pela tesouraria.
|
||||
* Usa Instant para timestamps.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "treasury_payment", indexes = {
|
||||
@Index(name = "idx_treasury_payment_order", columnList = "payment_order_id"),
|
||||
@Index(name = "idx_treasury_payment_status", columnList = "status"),
|
||||
@Index(name = "idx_treasury_payment_paid", columnList = "paid_at")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class TreasuryPayment extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "payment_order_id", nullable = false)
|
||||
private PaymentOrder paymentOrder;
|
||||
|
||||
@Column(name = "paid_at")
|
||||
private Instant paidAt; // Data/hora do pagamento efetivo
|
||||
|
||||
@Column(length = 100, name = "transaction_ref")
|
||||
private String transactionRef; // Referência da transação bancária
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "PENDING"; // PENDING, PAID, REJECTED, CANCELLED - preparado para enum
|
||||
|
||||
@Column(length = 1000)
|
||||
private String message; // Mensagem de retorno do banco ou observações
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package br.gov.sigefp.treasury.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;
|
||||
|
||||
/**
|
||||
* Entidade que representa um Plano de Tesouraria (PT).
|
||||
* Define tetos de pagamento para períodos específicos (mensais).
|
||||
* Conforme Master Plan - Module 2: Preventive Control.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "treasury_plan", indexes = {
|
||||
@Index(name = "idx_treasury_plan_fiscal_year", columnList = "fiscal_year,reference_month"),
|
||||
@Index(name = "idx_treasury_plan_status", columnList = "status"),
|
||||
@Index(name = "idx_treasury_plan_period", columnList = "start_date,end_date")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuperBuilder
|
||||
public class TreasuryPlan extends BaseEntity {
|
||||
|
||||
@Column(name = "fiscal_year", nullable = false)
|
||||
private Integer fiscalYear; // Ex: 2025
|
||||
|
||||
@Column(name = "reference_month", nullable = false)
|
||||
private Integer referenceMonth; // 1-12
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT"; // DRAFT, APPROVED, CLOSED
|
||||
|
||||
@Column(name = "approved_ceiling", nullable = false, precision = 19, scale = 2)
|
||||
private BigDecimal approvedCeiling; // Teto aprovado para o período
|
||||
|
||||
@Column(name = "executed_amount", nullable = false, precision = 19, scale = 2)
|
||||
@Builder.Default
|
||||
private BigDecimal executedAmount = BigDecimal.ZERO; // Atualizado via listeners
|
||||
|
||||
@Column(name = "start_date", nullable = false)
|
||||
private LocalDate startDate; // Data de início do período
|
||||
|
||||
@Column(name = "end_date", nullable = false)
|
||||
private LocalDate endDate; // Data de fim do período
|
||||
|
||||
@Column(name = "approved_by")
|
||||
private java.util.UUID approvedBy; // Usuário que aprovou o plano
|
||||
|
||||
@Column(name = "approved_at")
|
||||
private java.time.LocalDateTime approvedAt;
|
||||
|
||||
/**
|
||||
* Calcula o valor disponível (teto - executado).
|
||||
*/
|
||||
public BigDecimal getAvailableAmount() {
|
||||
return approvedCeiling.subtract(executedAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se uma data está dentro do período do plano.
|
||||
*/
|
||||
public boolean isDateWithinPeriod(LocalDate date) {
|
||||
return !date.isBefore(startDate) && !date.isAfter(endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica se o plano está aprovado e ativo.
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return "APPROVED".equals(status);
|
||||
}
|
||||
}
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package br.gov.sigefp.treasury.integration;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.domain.PaymentOrder;
|
||||
import br.gov.sigefp.treasury.repository.PaymentOrderRepository;
|
||||
import br.gov.sigefp.treasury.service.CashAccountService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Serviço de integração do módulo Tesouro com outros módulos.
|
||||
* Similar a BudgetIntegrationService no módulo de Orçamento.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class TreasuryIntegrationService {
|
||||
|
||||
private final PaymentOrderRepository paymentOrderRepository;
|
||||
private final CashAccountService cashAccountService;
|
||||
|
||||
/**
|
||||
* Valida se uma ordem de pagamento pode ser criada.
|
||||
* Verifica disponibilidade de caixa e integridade dos dados.
|
||||
*/
|
||||
public void validatePaymentOrder(UUID paymentOrderId) {
|
||||
PaymentOrder paymentOrder = paymentOrderRepository.findById(paymentOrderId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Ordem de pagamento não encontrada: " + paymentOrderId));
|
||||
|
||||
// Validações básicas
|
||||
if (paymentOrder.getNetAmount() == null || paymentOrder.getNetAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new BusinessException(
|
||||
"Valor líquido da ordem de pagamento deve ser maior que zero",
|
||||
"INVALID_AMOUNT",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Validação de disponibilidade de caixa será feita quando a ordem for programada
|
||||
// (requer cashAccountId na PaymentOrder, que pode ser adicionado no futuro)
|
||||
|
||||
log.debug("Ordem de pagamento validada: paymentOrderId={}", paymentOrderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida disponibilidade de caixa para um pagamento.
|
||||
*/
|
||||
public void validateCashAvailability(UUID cashAccountId, BigDecimal amount) {
|
||||
if (cashAccountId == null) {
|
||||
throw new BusinessException(
|
||||
"Conta de caixa é obrigatória para validação",
|
||||
"MISSING_CASH_ACCOUNT",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
BigDecimal available = cashAccountService.getAvailableBalance(cashAccountId);
|
||||
|
||||
if (amount.compareTo(available) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Saldo disponível insuficiente. Disponível: %s, Solicitado: %s",
|
||||
available, amount),
|
||||
"INSUFFICIENT_CASH",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
log.debug("Disponibilidade de caixa validada: accountId={}, amount={}, available={}",
|
||||
cashAccountId, amount, available);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra a execução de um pagamento.
|
||||
* Atualiza saldos e cria entradas de tesouraria.
|
||||
*/
|
||||
public void registerPaymentExecution(UUID paymentOrderId, BigDecimal amount, UUID cashAccountId) {
|
||||
PaymentOrder paymentOrder = paymentOrderRepository.findById(paymentOrderId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Ordem de pagamento não encontrada: " + paymentOrderId));
|
||||
|
||||
// Validar disponibilidade
|
||||
validateCashAvailability(cashAccountId, amount);
|
||||
|
||||
// Atualizar saldo da conta
|
||||
cashAccountService.updateBalance(cashAccountId, amount, "OUTFLOW");
|
||||
|
||||
log.info("Execução de pagamento registrada: paymentOrderId={}, amount={}, accountId={}",
|
||||
paymentOrderId, amount, cashAccountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza execução orçamentária após pagamento.
|
||||
* Este método será chamado pelo TreasuryPaymentService quando um pagamento for confirmado.
|
||||
*/
|
||||
public void updateBudgetExecution(UUID paymentOrderId) {
|
||||
PaymentOrder paymentOrder = paymentOrderRepository.findById(paymentOrderId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Ordem de pagamento não encontrada: " + paymentOrderId));
|
||||
|
||||
// A atualização da execução orçamentária já é feita pelo TreasuryPaymentService
|
||||
// Este método pode ser usado para validações adicionais ou notificações
|
||||
|
||||
log.debug("Execução orçamentária atualizada para ordem de pagamento: paymentOrderId={}",
|
||||
paymentOrderId);
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.Approval;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface ApprovalRepository extends JpaRepository<Approval, UUID> {
|
||||
|
||||
List<Approval> findByAuthorizationId(UUID authorizationId);
|
||||
|
||||
List<Approval> findByApprovedBy(UUID approvedBy);
|
||||
|
||||
List<Approval> findByAuthorizationIdOrderByLevelAsc(UUID authorizationId);
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.BankReconciliation;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface BankReconciliationRepository extends JpaRepository<BankReconciliation, UUID> {
|
||||
|
||||
@Query("SELECT br FROM BankReconciliation br WHERE br.cashAccount.id = :cashAccountId")
|
||||
List<BankReconciliation> findByCashAccountId(@Param("cashAccountId") UUID cashAccountId);
|
||||
|
||||
List<BankReconciliation> findByStatus(String status);
|
||||
|
||||
@Query("SELECT br FROM BankReconciliation br WHERE br.cashAccount.id = :cashAccountId " +
|
||||
"AND br.reconciliationDate BETWEEN :startDate AND :endDate")
|
||||
List<BankReconciliation> findByCashAccountIdAndDateRange(
|
||||
@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate
|
||||
);
|
||||
|
||||
@Query("SELECT br FROM BankReconciliation br WHERE br.cashAccount.id = :cashAccountId " +
|
||||
"AND br.status = 'PENDING' ORDER BY br.reconciliationDate DESC")
|
||||
List<BankReconciliation> findPendingByCashAccount(@Param("cashAccountId") UUID cashAccountId);
|
||||
|
||||
@Query("SELECT br FROM BankReconciliation br WHERE br.cashAccount.id = :cashAccountId " +
|
||||
"ORDER BY br.reconciliationDate DESC")
|
||||
Optional<BankReconciliation> findFirstByCashAccountIdOrderByReconciliationDateDesc(@Param("cashAccountId") UUID cashAccountId);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface CashAccountRepository extends JpaRepository<CashAccount, UUID> {
|
||||
|
||||
Optional<CashAccount> findByCode(String code);
|
||||
|
||||
List<CashAccount> findByType(String type);
|
||||
|
||||
List<CashAccount> findByIsActiveTrue();
|
||||
|
||||
List<CashAccount> findByBankId(UUID bankId);
|
||||
|
||||
@Query("SELECT ca FROM CashAccount ca WHERE ca.type = 'BANK_ACCOUNT' AND ca.isActive = true")
|
||||
List<CashAccount> findActiveBankAccounts();
|
||||
|
||||
@Query("SELECT ca FROM CashAccount ca WHERE ca.type = 'CASH' AND ca.isActive = true")
|
||||
List<CashAccount> findActiveCashAccounts();
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.CashFlow;
|
||||
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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface CashFlowRepository extends JpaRepository<CashFlow, UUID> {
|
||||
|
||||
@Query("SELECT cf FROM CashFlow cf WHERE cf.cashAccount.id = :cashAccountId")
|
||||
Page<CashFlow> findByCashAccountId(@Param("cashAccountId") UUID cashAccountId, Pageable pageable);
|
||||
|
||||
@Query("SELECT cf FROM CashFlow cf WHERE cf.cashAccount.id = :cashAccountId " +
|
||||
"AND cf.transactionDate BETWEEN :startDate AND :endDate")
|
||||
Page<CashFlow> findByCashAccountIdAndDateRange(
|
||||
@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
List<CashFlow> findByType(String type);
|
||||
|
||||
@Query("SELECT cf FROM CashFlow cf WHERE cf.cashAccount.id = :cashAccountId AND cf.type = :type")
|
||||
List<CashFlow> findByCashAccountIdAndType(@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("type") String type);
|
||||
|
||||
@Query("SELECT SUM(cf.amount) FROM CashFlow cf WHERE cf.cashAccount.id = :cashAccountId " +
|
||||
"AND cf.type = :type AND cf.transactionDate BETWEEN :startDate AND :endDate")
|
||||
java.math.BigDecimal calculateTotalByAccountAndType(
|
||||
@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("type") String type,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate
|
||||
);
|
||||
|
||||
@Query("SELECT cf FROM CashFlow cf WHERE cf.referenceType = :referenceType " +
|
||||
"AND cf.referenceId = :referenceId")
|
||||
List<CashFlow> findByReference(@Param("referenceType") String referenceType,
|
||||
@Param("referenceId") UUID referenceId);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.PaymentAuthorization;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface PaymentAuthorizationRepository extends JpaRepository<PaymentAuthorization, UUID> {
|
||||
|
||||
List<PaymentAuthorization> findByPaymentOrderId(UUID paymentOrderId);
|
||||
|
||||
List<PaymentAuthorization> findByPaymentBatchId(UUID paymentBatchId);
|
||||
|
||||
List<PaymentAuthorization> findByStatus(String status);
|
||||
|
||||
List<PaymentAuthorization> findByRequestedBy(UUID requestedBy);
|
||||
|
||||
@Query("SELECT pa FROM PaymentAuthorization pa WHERE pa.status = 'PENDING' " +
|
||||
"AND pa.currentApprovalLevel <= :maxLevel")
|
||||
List<PaymentAuthorization> findPendingApprovals(@Param("maxLevel") Integer maxLevel);
|
||||
|
||||
@Query("SELECT pa FROM PaymentAuthorization pa WHERE pa.status = 'PENDING' " +
|
||||
"AND pa.currentApprovalLevel = :level")
|
||||
List<PaymentAuthorization> findPendingByLevel(@Param("level") Integer level);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.PaymentBatch;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface PaymentBatchRepository extends JpaRepository<PaymentBatch, UUID> {
|
||||
|
||||
List<PaymentBatch> findByPeriodId(Long periodId);
|
||||
|
||||
List<PaymentBatch> findByMinistryId(UUID ministryId);
|
||||
|
||||
List<PaymentBatch> findByStatus(String status);
|
||||
|
||||
@Query("SELECT pb FROM PaymentBatch pb WHERE pb.periodId = :periodId AND pb.ministryId = :ministryId")
|
||||
List<PaymentBatch> findByPeriodIdAndMinistryId(@Param("periodId") Long periodId, @Param("ministryId") UUID ministryId);
|
||||
|
||||
@Query("SELECT pb FROM PaymentBatch pb WHERE pb.periodId = :periodId AND pb.status = :status")
|
||||
List<PaymentBatch> findByPeriodIdAndStatus(@Param("periodId") Long periodId, @Param("status") String status);
|
||||
|
||||
@Query("SELECT pb FROM PaymentBatch pb WHERE pb.ministryId = :ministryId AND pb.status = :status")
|
||||
List<PaymentBatch> findByMinistryIdAndStatus(@Param("ministryId") UUID ministryId, @Param("status") String status);
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.PaymentOrder;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface PaymentOrderRepository extends JpaRepository<PaymentOrder, UUID> {
|
||||
|
||||
List<PaymentOrder> findByPaymentBatchId(UUID paymentBatchId);
|
||||
|
||||
List<PaymentOrder> findByStatus(String status);
|
||||
|
||||
@Query("SELECT po FROM PaymentOrder po WHERE po.paymentBatch.id = :batchId AND po.status = :status")
|
||||
List<PaymentOrder> findByPaymentBatchIdAndStatus(@Param("batchId") UUID batchId, @Param("status") String status);
|
||||
|
||||
@Query("SELECT po FROM PaymentOrder po WHERE po.payrollRunId = :payrollRunId")
|
||||
List<PaymentOrder> findByPayrollRunId(@Param("payrollRunId") UUID payrollRunId);
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.ReconciliationItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface ReconciliationItemRepository extends JpaRepository<ReconciliationItem, UUID> {
|
||||
|
||||
List<ReconciliationItem> findByReconciliationId(UUID reconciliationId);
|
||||
|
||||
List<ReconciliationItem> findByMatchStatus(String matchStatus);
|
||||
|
||||
List<ReconciliationItem> findByReconciliationIdAndMatchStatus(UUID reconciliationId, String matchStatus);
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntry;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface TreasuryEntryRepository extends JpaRepository<TreasuryEntry, UUID> {
|
||||
|
||||
@Query("SELECT te FROM TreasuryEntry te WHERE te.cashAccount.id = :cashAccountId")
|
||||
Page<TreasuryEntry> findByCashAccountId(@Param("cashAccountId") UUID cashAccountId, Pageable pageable);
|
||||
|
||||
Page<TreasuryEntry> findByType(TreasuryEntryType type, Pageable pageable);
|
||||
|
||||
Page<TreasuryEntry> findByStatus(String status, Pageable pageable);
|
||||
|
||||
@Query("SELECT te FROM TreasuryEntry te WHERE te.cashAccount.id = :cashAccountId AND te.type = :type")
|
||||
Page<TreasuryEntry> findByCashAccountIdAndType(@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("type") TreasuryEntryType type, Pageable pageable);
|
||||
|
||||
@Query("SELECT te FROM TreasuryEntry te WHERE te.cashAccount.id = :cashAccountId AND te.status = :status")
|
||||
Page<TreasuryEntry> findByCashAccountIdAndStatus(@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("status") String status, Pageable pageable);
|
||||
|
||||
@Query("SELECT te FROM TreasuryEntry te WHERE te.paymentOrderId = :paymentOrderId")
|
||||
List<TreasuryEntry> findByPaymentOrderId(@Param("paymentOrderId") UUID paymentOrderId);
|
||||
|
||||
@Query("SELECT te FROM TreasuryEntry te WHERE te.paymentBatchId = :paymentBatchId")
|
||||
List<TreasuryEntry> findByPaymentBatchId(@Param("paymentBatchId") UUID paymentBatchId);
|
||||
|
||||
@Query("SELECT te FROM TreasuryEntry te WHERE te.cashAccount.id = :cashAccountId " +
|
||||
"AND te.transactionDate BETWEEN :startDate AND :endDate")
|
||||
List<TreasuryEntry> findByCashAccountIdAndDateRange(
|
||||
@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate
|
||||
);
|
||||
|
||||
@Query("SELECT SUM(te.amount) FROM TreasuryEntry te WHERE te.cashAccount.id = :cashAccountId " +
|
||||
"AND te.type IN :types AND te.status = 'EXECUTED'")
|
||||
java.math.BigDecimal calculateTotalByCashAccountAndTypes(
|
||||
@Param("cashAccountId") UUID cashAccountId,
|
||||
@Param("types") List<TreasuryEntryType> types
|
||||
);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.TreasuryPayment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface TreasuryPaymentRepository extends JpaRepository<TreasuryPayment, UUID> {
|
||||
|
||||
List<TreasuryPayment> findByPaymentOrderId(UUID paymentOrderId);
|
||||
|
||||
List<TreasuryPayment> findByStatus(String status);
|
||||
|
||||
@Query("SELECT tp FROM TreasuryPayment tp WHERE tp.paymentOrder.id = :paymentOrderId AND tp.status = :status")
|
||||
List<TreasuryPayment> findByPaymentOrderIdAndStatus(@Param("paymentOrderId") UUID paymentOrderId, @Param("status") String status);
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package br.gov.sigefp.treasury.repository;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.TreasuryPlan;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository para gestão de Planos de Tesouraria.
|
||||
*/
|
||||
@Repository
|
||||
public interface TreasuryPlanRepository extends JpaRepository<TreasuryPlan, UUID> {
|
||||
|
||||
/**
|
||||
* Encontra o plano ativo para uma data específica.
|
||||
*/
|
||||
@Query("SELECT tp FROM TreasuryPlan tp WHERE tp.status = 'APPROVED' " +
|
||||
"AND :date >= tp.startDate AND :date <= tp.endDate")
|
||||
Optional<TreasuryPlan> findActivePlanForDate(@Param("date") LocalDate date);
|
||||
|
||||
/**
|
||||
* Encontra todos os planos aprovados para um período.
|
||||
*/
|
||||
@Query("SELECT tp FROM TreasuryPlan tp WHERE tp.status = 'APPROVED' " +
|
||||
"AND tp.startDate <= :endDate AND tp.endDate >= :startDate")
|
||||
List<TreasuryPlan> findApprovedPlansForPeriod(@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate);
|
||||
|
||||
/**
|
||||
* Encontra planos por status.
|
||||
*/
|
||||
List<TreasuryPlan> findByStatus(String status);
|
||||
|
||||
/**
|
||||
* Encontra planos por ano fiscal e mês de referência.
|
||||
*/
|
||||
@Query("SELECT tp FROM TreasuryPlan tp WHERE tp.fiscalYear = :fiscalYear " +
|
||||
"AND tp.referenceMonth = :referenceMonth")
|
||||
Optional<TreasuryPlan> findByFiscalYearAndReferenceMonth(@Param("fiscalYear") Integer fiscalYear,
|
||||
@Param("referenceMonth") Integer referenceMonth);
|
||||
|
||||
/**
|
||||
* Calcula o total executado para um plano específico.
|
||||
* Soma todos os pagamentos autorizados relacionados ao período do plano.
|
||||
*/
|
||||
@Query("SELECT COALESCE(SUM(po.grossAmount), 0) FROM PaymentOrder po " +
|
||||
"WHERE po.paymentBatch.id IN " +
|
||||
"(SELECT pb.id FROM PaymentBatch pb WHERE pb.status = 'SENT_TO_BANK' OR pb.status = 'EXECUTED') " +
|
||||
"AND po.createdAt >= :startDate AND po.createdAt <= :endDate")
|
||||
java.math.BigDecimal calculateExecutedAmountForPeriod(@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate);
|
||||
}
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.BankReconciliationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateBankReconciliationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.ReconciliationItemDTO;
|
||||
import br.gov.sigefp.treasury.domain.BankReconciliation;
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.domain.ReconciliationItem;
|
||||
import br.gov.sigefp.treasury.repository.BankReconciliationRepository;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
import br.gov.sigefp.treasury.repository.ReconciliationItemRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de conciliação bancária.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class BankReconciliationService {
|
||||
|
||||
private final BankReconciliationRepository reconciliationRepository;
|
||||
private final CashAccountRepository cashAccountRepository;
|
||||
private final ReconciliationItemRepository reconciliationItemRepository;
|
||||
private final CashAccountService cashAccountService;
|
||||
|
||||
public BankReconciliationDTO importStatement(CreateBankReconciliationDTO dto) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(dto.getCashAccountId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conta não encontrada: " + dto.getCashAccountId()));
|
||||
|
||||
BigDecimal systemBalance = cashAccountService.getCurrentBalance(dto.getCashAccountId());
|
||||
BigDecimal difference = dto.getStatementBalance().subtract(systemBalance);
|
||||
|
||||
BankReconciliation reconciliation = BankReconciliation.builder()
|
||||
.cashAccount(cashAccount)
|
||||
.reconciliationDate(dto.getReconciliationDate())
|
||||
.statementBalance(dto.getStatementBalance())
|
||||
.systemBalance(systemBalance)
|
||||
.difference(difference)
|
||||
.status("PENDING")
|
||||
.build();
|
||||
|
||||
BankReconciliation saved = reconciliationRepository.save(reconciliation);
|
||||
|
||||
// Criar itens de conciliação se fornecidos
|
||||
if (dto.getReconciliationItems() != null && !dto.getReconciliationItems().isEmpty()) {
|
||||
for (ReconciliationItemDTO itemDTO : dto.getReconciliationItems()) {
|
||||
ReconciliationItem item = ReconciliationItem.builder()
|
||||
.reconciliation(saved)
|
||||
.transactionDate(itemDTO.getTransactionDate())
|
||||
.description(itemDTO.getDescription())
|
||||
.statementAmount(itemDTO.getStatementAmount())
|
||||
.systemAmount(itemDTO.getSystemAmount())
|
||||
.matchStatus("PENDING")
|
||||
.build();
|
||||
reconciliationItemRepository.save(item);
|
||||
saved.getReconciliationItems().add(item);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Conciliação bancária criada: id={}, accountId={}, difference={}",
|
||||
saved.getId(), dto.getCashAccountId(), difference);
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public BankReconciliationDTO reconcile(UUID reconciliationId) {
|
||||
BankReconciliation reconciliation = reconciliationRepository.findById(reconciliationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conciliação não encontrada: " + reconciliationId));
|
||||
|
||||
if (!"PENDING".equals(reconciliation.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Apenas conciliações pendentes podem ser reconciliadas",
|
||||
"INVALID_STATUS",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
// Tentar matching automático
|
||||
performAutomaticMatching(reconciliation);
|
||||
|
||||
// Verificar se todos os itens foram reconciliados
|
||||
long unmatchedCount = reconciliation.getReconciliationItems().stream()
|
||||
.filter(item -> !"MATCHED".equals(item.getMatchStatus()))
|
||||
.count();
|
||||
|
||||
if (unmatchedCount == 0 && reconciliation.getDifference().compareTo(BigDecimal.ZERO) == 0) {
|
||||
reconciliation.setStatus("RECONCILED");
|
||||
} else if (reconciliation.getDifference().compareTo(BigDecimal.ZERO) != 0) {
|
||||
reconciliation.setStatus("DISCREPANCY");
|
||||
}
|
||||
|
||||
BankReconciliation saved = reconciliationRepository.save(reconciliation);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public void matchItem(UUID reconciliationId, UUID itemId, UUID systemTransactionId) {
|
||||
BankReconciliation reconciliation = reconciliationRepository.findById(reconciliationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conciliação não encontrada: " + reconciliationId));
|
||||
|
||||
ReconciliationItem item = reconciliationItemRepository.findById(itemId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Item não encontrado: " + itemId));
|
||||
|
||||
if (!item.getReconciliation().getId().equals(reconciliationId)) {
|
||||
throw new BusinessException(
|
||||
"Item não pertence a esta conciliação",
|
||||
"INVALID_ITEM",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
item.setMatchStatus("MATCHED");
|
||||
item.setMatchedTransactionId(systemTransactionId);
|
||||
reconciliationItemRepository.save(item);
|
||||
|
||||
log.info("Item de conciliação correspondido: itemId={}, transactionId={}",
|
||||
itemId, systemTransactionId);
|
||||
}
|
||||
|
||||
public BankReconciliationDTO finalize(UUID reconciliationId, UUID reconciledBy) {
|
||||
BankReconciliation reconciliation = reconciliationRepository.findById(reconciliationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conciliação não encontrada: " + reconciliationId));
|
||||
|
||||
if (!"RECONCILED".equals(reconciliation.getStatus()) &&
|
||||
!"DISCREPANCY".equals(reconciliation.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Apenas conciliações reconciliadas ou com discrepância podem ser finalizadas",
|
||||
"INVALID_STATUS",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
reconciliation.setStatus("RECONCILED");
|
||||
reconciliation.setReconciledBy(reconciledBy);
|
||||
reconciliation.setReconciledAt(LocalDateTime.now());
|
||||
|
||||
// Atualizar saldo da conta se houver diferença ajustada
|
||||
if (reconciliation.getDifference().compareTo(BigDecimal.ZERO) != 0) {
|
||||
// Ajustar saldo do sistema para corresponder ao extrato
|
||||
cashAccountService.updateBalance(
|
||||
reconciliation.getCashAccount().getId(),
|
||||
reconciliation.getDifference(),
|
||||
reconciliation.getDifference().compareTo(BigDecimal.ZERO) > 0 ? "INFLOW" : "OUTFLOW");
|
||||
}
|
||||
|
||||
BankReconciliation saved = reconciliationRepository.save(reconciliation);
|
||||
log.info("Conciliação finalizada: id={}, reconciledBy={}", reconciliationId, reconciledBy);
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BankReconciliationDTO findById(UUID id) {
|
||||
BankReconciliation reconciliation = reconciliationRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conciliação não encontrada: " + id));
|
||||
return toDTO(reconciliation);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BankReconciliationDTO> findByCashAccountId(UUID cashAccountId) {
|
||||
return reconciliationRepository.findByCashAccountId(cashAccountId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ReconciliationItemDTO> findUnmatchedItems(UUID reconciliationId) {
|
||||
BankReconciliation reconciliation = reconciliationRepository.findById(reconciliationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conciliação não encontrada: " + reconciliationId));
|
||||
|
||||
return reconciliation.getReconciliationItems().stream()
|
||||
.filter(item -> !"MATCHED".equals(item.getMatchStatus()))
|
||||
.map(this::itemToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza matching automático de itens baseado em data e valor.
|
||||
*/
|
||||
private void performAutomaticMatching(BankReconciliation reconciliation) {
|
||||
// Implementação simplificada: matching por data e valor exato
|
||||
// Em produção, poderia usar algoritmos mais sofisticados
|
||||
for (ReconciliationItem item : reconciliation.getReconciliationItems()) {
|
||||
if ("PENDING".equals(item.getMatchStatus()) && item.getSystemAmount() != null) {
|
||||
// Verificar se há correspondência exata
|
||||
if (item.getStatementAmount() != null &&
|
||||
item.getStatementAmount().compareTo(item.getSystemAmount()) == 0) {
|
||||
item.setMatchStatus("MATCHED");
|
||||
reconciliationItemRepository.save(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BankReconciliationDTO toDTO(BankReconciliation reconciliation) {
|
||||
List<ReconciliationItemDTO> items = reconciliation.getReconciliationItems().stream()
|
||||
.map(this::itemToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return BankReconciliationDTO.builder()
|
||||
.id(reconciliation.getId())
|
||||
.cashAccountId(reconciliation.getCashAccount().getId())
|
||||
.reconciliationDate(reconciliation.getReconciliationDate())
|
||||
.statementBalance(reconciliation.getStatementBalance())
|
||||
.systemBalance(reconciliation.getSystemBalance())
|
||||
.difference(reconciliation.getDifference())
|
||||
.status(reconciliation.getStatus())
|
||||
.reconciledBy(reconciliation.getReconciledBy())
|
||||
.reconciledAt(reconciliation.getReconciledAt())
|
||||
.items(items)
|
||||
.createdAt(reconciliation.getCreatedAt())
|
||||
.updatedAt(reconciliation.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private ReconciliationItemDTO itemToDTO(ReconciliationItem item) {
|
||||
return ReconciliationItemDTO.builder()
|
||||
.id(item.getId())
|
||||
.reconciliationId(item.getReconciliation().getId())
|
||||
.transactionDate(item.getTransactionDate())
|
||||
.description(item.getDescription())
|
||||
.statementAmount(item.getStatementAmount())
|
||||
.systemAmount(item.getSystemAmount())
|
||||
.matchStatus(item.getMatchStatus())
|
||||
.matchedTransactionId(item.getMatchedTransactionId())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.CashAccountDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateCashAccountDTO;
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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 contas de caixa e bancárias.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class CashAccountService {
|
||||
|
||||
private final CashAccountRepository cashAccountRepository;
|
||||
|
||||
public CashAccountDTO create(CreateCashAccountDTO dto) {
|
||||
// Validar código único
|
||||
if (cashAccountRepository.findByCode(dto.getCode()).isPresent()) {
|
||||
throw new BusinessException(
|
||||
"Já existe uma conta com o código: " + dto.getCode(),
|
||||
"DUPLICATE_CODE",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
// Validar se bankId é obrigatório para BANK_ACCOUNT
|
||||
if ("BANK_ACCOUNT".equals(dto.getType()) && dto.getBankId() == null) {
|
||||
throw new BusinessException(
|
||||
"BankId é obrigatório para contas bancárias",
|
||||
"MISSING_BANK_ID",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
CashAccount cashAccount = CashAccount.builder()
|
||||
.code(dto.getCode())
|
||||
.name(dto.getName())
|
||||
.type(dto.getType())
|
||||
.orgUnitId(dto.getOrgUnitId())
|
||||
.bankId(dto.getBankId())
|
||||
.accountNumber(dto.getAccountNumber())
|
||||
.branchCode(dto.getBranchCode())
|
||||
.currency(dto.getCurrency() != null ? dto.getCurrency() : "XOF")
|
||||
.isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
|
||||
.currentBalance(BigDecimal.ZERO)
|
||||
.availableBalance(BigDecimal.ZERO)
|
||||
.build();
|
||||
|
||||
CashAccount saved = cashAccountRepository.save(cashAccount);
|
||||
log.info("Conta de caixa criada: id={}, code={}, name={}", saved.getId(), saved.getCode(), saved.getName());
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CashAccountDTO findById(UUID id) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + id));
|
||||
return toDTO(cashAccount);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CashAccountDTO> findAll() {
|
||||
return cashAccountRepository.findAll().stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CashAccountDTO> findActive() {
|
||||
return cashAccountRepository.findByIsActiveTrue().stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CashAccountDTO> findByType(String type) {
|
||||
return cashAccountRepository.findByType(type).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal getAvailableBalance(UUID cashAccountId) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
|
||||
return cashAccount.getAvailableBalance();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal getCurrentBalance(UUID cashAccountId) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
|
||||
return cashAccount.getCurrentBalance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza o saldo da conta após uma operação.
|
||||
*
|
||||
* @param cashAccountId ID da conta
|
||||
* @param amount Valor da operação (positivo para entrada, negativo para
|
||||
* saída)
|
||||
* @param operation Tipo de operação (INFLOW, OUTFLOW)
|
||||
*/
|
||||
public void updateBalance(UUID cashAccountId, BigDecimal amount, String operation) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
|
||||
|
||||
if ("INFLOW".equals(operation)) {
|
||||
cashAccount.setCurrentBalance(cashAccount.getCurrentBalance().add(amount));
|
||||
cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().add(amount));
|
||||
} else if ("OUTFLOW".equals(operation)) {
|
||||
cashAccount.setCurrentBalance(cashAccount.getCurrentBalance().subtract(amount));
|
||||
cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().subtract(amount));
|
||||
}
|
||||
|
||||
cashAccountRepository.save(cashAccount);
|
||||
log.debug("Saldo atualizado: accountId={}, operation={}, amount={}, newBalance={}",
|
||||
cashAccountId, operation, amount, cashAccount.getCurrentBalance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compromete saldo disponível (reduz availableBalance sem alterar
|
||||
* currentBalance).
|
||||
* Usado quando um pagamento é programado mas ainda não executado.
|
||||
*/
|
||||
public void commitBalance(UUID cashAccountId, BigDecimal amount) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
|
||||
|
||||
if (cashAccount.getAvailableBalance().compareTo(amount) < 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Saldo disponível insuficiente. Disponível: %s, Solicitado: %s",
|
||||
cashAccount.getAvailableBalance(), amount),
|
||||
"INSUFFICIENT_BALANCE",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().subtract(amount));
|
||||
cashAccountRepository.save(cashAccount);
|
||||
log.debug("Saldo comprometido: accountId={}, amount={}, newAvailableBalance={}",
|
||||
cashAccountId, amount, cashAccount.getAvailableBalance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Libera saldo comprometido (aumenta availableBalance).
|
||||
* Usado quando um pagamento programado é cancelado.
|
||||
*/
|
||||
public void releaseBalance(UUID cashAccountId, BigDecimal amount) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
|
||||
|
||||
cashAccount.setAvailableBalance(cashAccount.getAvailableBalance().add(amount));
|
||||
cashAccountRepository.save(cashAccount);
|
||||
log.debug("Saldo liberado: accountId={}, amount={}, newAvailableBalance={}",
|
||||
cashAccountId, amount, cashAccount.getAvailableBalance());
|
||||
}
|
||||
|
||||
private CashAccountDTO toDTO(CashAccount cashAccount) {
|
||||
return CashAccountDTO.builder()
|
||||
.id(cashAccount.getId())
|
||||
.code(cashAccount.getCode())
|
||||
.name(cashAccount.getName())
|
||||
.type(cashAccount.getType())
|
||||
.bankId(cashAccount.getBankId())
|
||||
.accountNumber(cashAccount.getAccountNumber())
|
||||
.branchCode(cashAccount.getBranchCode())
|
||||
.currency(cashAccount.getCurrency())
|
||||
.isActive(cashAccount.getIsActive())
|
||||
.currentBalance(cashAccount.getCurrentBalance())
|
||||
.availableBalance(cashAccount.getAvailableBalance())
|
||||
.createdAt(cashAccount.getCreatedAt())
|
||||
.updatedAt(cashAccount.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.CashFlowDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateCashFlowDTO;
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.domain.CashFlow;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
import br.gov.sigefp.treasury.repository.CashFlowRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de fluxo de caixa.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class CashFlowService {
|
||||
|
||||
private final CashFlowRepository cashFlowRepository;
|
||||
private final CashAccountRepository cashAccountRepository;
|
||||
private final CashAccountService cashAccountService;
|
||||
|
||||
public CashFlowDTO registerFlow(CreateCashFlowDTO dto) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(dto.getCashAccountId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conta não encontrada: " + dto.getCashAccountId()));
|
||||
|
||||
// Calcular saldo após a transação
|
||||
BigDecimal currentBalance = cashAccount.getCurrentBalance();
|
||||
BigDecimal balanceAfter;
|
||||
|
||||
if ("INFLOW".equals(dto.getType())) {
|
||||
balanceAfter = currentBalance.add(dto.getAmount());
|
||||
} else {
|
||||
balanceAfter = currentBalance.subtract(dto.getAmount());
|
||||
}
|
||||
|
||||
CashFlow cashFlow = CashFlow.builder()
|
||||
.cashAccount(cashAccount)
|
||||
.transactionDate(dto.getTransactionDate())
|
||||
.type(dto.getType())
|
||||
.amount(dto.getAmount())
|
||||
.description(dto.getDescription())
|
||||
.referenceId(dto.getReferenceId())
|
||||
.referenceType(dto.getReferenceType())
|
||||
.balanceAfter(balanceAfter)
|
||||
.build();
|
||||
|
||||
CashFlow saved = cashFlowRepository.save(cashFlow);
|
||||
|
||||
// Atualizar saldo da conta
|
||||
cashAccountService.updateBalance(dto.getCashAccountId(), dto.getAmount(), dto.getType());
|
||||
|
||||
log.info("Fluxo de caixa registrado: id={}, accountId={}, type={}, amount={}",
|
||||
saved.getId(), dto.getCashAccountId(), dto.getType(), dto.getAmount());
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CashFlowDTO findById(UUID id) {
|
||||
CashFlow cashFlow = cashFlowRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Fluxo não encontrado: " + id));
|
||||
return toDTO(cashFlow);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<CashFlowDTO> findByCashAccountId(UUID cashAccountId, Pageable pageable) {
|
||||
return cashFlowRepository.findByCashAccountId(cashAccountId, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<CashFlowDTO> findByCashAccountIdAndDateRange(
|
||||
UUID cashAccountId, LocalDate startDate, LocalDate endDate, Pageable pageable) {
|
||||
return cashFlowRepository.findByCashAccountIdAndDateRange(
|
||||
cashAccountId, startDate, endDate, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal calculateProjectedBalance(UUID cashAccountId, LocalDate targetDate) {
|
||||
CashAccount cashAccount = cashAccountRepository.findById(cashAccountId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Conta não encontrada: " + cashAccountId));
|
||||
|
||||
BigDecimal currentBalance = cashAccount.getCurrentBalance();
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
if (targetDate.isBefore(today)) {
|
||||
return currentBalance; // Não projeta para o passado
|
||||
}
|
||||
|
||||
// Calcular projeção baseada em fluxos futuros programados
|
||||
// (assumindo que há uma forma de identificar fluxos programados)
|
||||
// Por enquanto, retorna o saldo atual
|
||||
return currentBalance;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, BigDecimal> getFlowSummary(UUID cashAccountId, LocalDate startDate, LocalDate endDate) {
|
||||
BigDecimal totalInflow = cashFlowRepository.calculateTotalByAccountAndType(
|
||||
cashAccountId, "INFLOW", startDate, endDate);
|
||||
BigDecimal totalOutflow = cashFlowRepository.calculateTotalByAccountAndType(
|
||||
cashAccountId, "OUTFLOW", startDate, endDate);
|
||||
|
||||
if (totalInflow == null) totalInflow = BigDecimal.ZERO;
|
||||
if (totalOutflow == null) totalOutflow = BigDecimal.ZERO;
|
||||
|
||||
return Map.of(
|
||||
"totalInflow", totalInflow,
|
||||
"totalOutflow", totalOutflow,
|
||||
"netFlow", totalInflow.subtract(totalOutflow)
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CashFlowDTO> findByReference(String referenceType, UUID referenceId) {
|
||||
return cashFlowRepository.findByReference(referenceType, referenceId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CashFlowDTO toDTO(CashFlow cashFlow) {
|
||||
return CashFlowDTO.builder()
|
||||
.id(cashFlow.getId())
|
||||
.cashAccountId(cashFlow.getCashAccount().getId())
|
||||
.transactionDate(cashFlow.getTransactionDate())
|
||||
.type(cashFlow.getType())
|
||||
.amount(cashFlow.getAmount())
|
||||
.description(cashFlow.getDescription())
|
||||
.referenceId(cashFlow.getReferenceId())
|
||||
.referenceType(cashFlow.getReferenceType())
|
||||
.balanceAfter(cashFlow.getBalanceAfter())
|
||||
.createdAt(cashFlow.getCreatedAt())
|
||||
.updatedAt(cashFlow.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.ApprovalDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentAuthorizationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentAuthorizationDTO;
|
||||
import br.gov.sigefp.treasury.domain.Approval;
|
||||
import br.gov.sigefp.treasury.domain.PaymentAuthorization;
|
||||
import br.gov.sigefp.treasury.repository.ApprovalRepository;
|
||||
import br.gov.sigefp.treasury.repository.PaymentAuthorizationRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de autorizações de pagamento.
|
||||
* Gerencia workflow hierárquico de aprovação.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class PaymentAuthorizationService {
|
||||
|
||||
private final PaymentAuthorizationRepository authorizationRepository;
|
||||
private final ApprovalRepository approvalRepository;
|
||||
private final TreasuryPlanService treasuryPlanService;
|
||||
private final br.gov.sigefp.treasury.service.PaymentOrderService paymentOrderService;
|
||||
|
||||
public PaymentAuthorizationDTO requestAuthorization(CreatePaymentAuthorizationDTO dto) {
|
||||
PaymentAuthorization authorization = PaymentAuthorization.builder()
|
||||
.paymentOrderId(dto.getPaymentOrderId())
|
||||
.paymentBatchId(dto.getPaymentBatchId())
|
||||
.requestedBy(dto.getRequestedBy())
|
||||
.requestedAt(LocalDateTime.now())
|
||||
.requiredApprovalLevel(dto.getRequiredApprovalLevel())
|
||||
.currentApprovalLevel(1)
|
||||
.status("PENDING")
|
||||
.build();
|
||||
|
||||
PaymentAuthorization saved = authorizationRepository.save(authorization);
|
||||
log.info("Autorização solicitada: id={}, requiredLevel={}, paymentOrderId={}",
|
||||
saved.getId(), saved.getRequiredApprovalLevel(), saved.getPaymentOrderId());
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public PaymentAuthorizationDTO approve(UUID authorizationId, UUID approverId, String comments) {
|
||||
PaymentAuthorization authorization = authorizationRepository.findById(authorizationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Autorização não encontrada: " + authorizationId));
|
||||
|
||||
if (!"PENDING".equals(authorization.getStatus()) &&
|
||||
!"PARTIALLY_APPROVED".equals(authorization.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Apenas autorizações pendentes ou parcialmente aprovadas podem ser aprovadas",
|
||||
"INVALID_STATUS",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
// Registrar aprovação
|
||||
Approval approval = Approval.builder()
|
||||
.authorization(authorization)
|
||||
.level(authorization.getCurrentApprovalLevel())
|
||||
.approvedBy(approverId)
|
||||
.approvedAt(LocalDateTime.now())
|
||||
.comments(comments)
|
||||
.build();
|
||||
|
||||
approvalRepository.save(approval);
|
||||
authorization.getApprovals().add(approval);
|
||||
|
||||
// Verificar se precisa de mais aprovações
|
||||
if (authorization.getCurrentApprovalLevel() >= authorization.getRequiredApprovalLevel()) {
|
||||
|
||||
// INTEGRAÇÃO TESOURO (Fase 3.1): Validar Teto Financeiro antes de Aprovar
|
||||
br.gov.sigefp.treasury.api.dto.PaymentOrderDTO paymentOrder = paymentOrderService
|
||||
.findById(authorization.getPaymentOrderId());
|
||||
|
||||
try {
|
||||
// 1. Validar disponibilidade
|
||||
treasuryPlanService.validateAvailability(paymentOrder.getGrossAmount());
|
||||
|
||||
// 2. Consumir cota (Atualizar valor executado)
|
||||
br.gov.sigefp.treasury.api.dto.TreasuryPlanDTO activePlan = treasuryPlanService
|
||||
.findActivePlanForDate(java.time.LocalDate.now());
|
||||
if (activePlan != null) {
|
||||
treasuryPlanService.updateExecutedAmount(activePlan.getId(), paymentOrder.getGrossAmount());
|
||||
}
|
||||
|
||||
authorization.setStatus("APPROVED");
|
||||
log.info("Autorização totalmente aprovada e teto consumido: id={}, planId={}, amount={}",
|
||||
authorizationId, activePlan != null ? activePlan.getId() : "N/A",
|
||||
paymentOrder.getGrossAmount());
|
||||
|
||||
} catch (BusinessException e) {
|
||||
if ("CEILING_EXCEEDED".equals(e.getCode())) {
|
||||
log.warn("Autorização rejeitada por falta de teto: id={}, amount={}", authorizationId,
|
||||
paymentOrder.getGrossAmount());
|
||||
throw e; // Repassa a exceção para o controller
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
} else {
|
||||
authorization.setCurrentApprovalLevel(authorization.getCurrentApprovalLevel() + 1);
|
||||
authorization.setStatus("PARTIALLY_APPROVED");
|
||||
log.info("Autorização parcialmente aprovada: id={}, currentLevel={}",
|
||||
authorizationId, authorization.getCurrentApprovalLevel());
|
||||
}
|
||||
|
||||
PaymentAuthorization saved = authorizationRepository.save(authorization);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public PaymentAuthorizationDTO reject(UUID authorizationId, UUID approverId, String reason) {
|
||||
PaymentAuthorization authorization = authorizationRepository.findById(authorizationId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Autorização não encontrada: " + authorizationId));
|
||||
|
||||
if ("APPROVED".equals(authorization.getStatus()) ||
|
||||
"REJECTED".equals(authorization.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Autorização já foi finalizada",
|
||||
"ALREADY_FINALIZED",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
authorization.setStatus("REJECTED");
|
||||
authorization.setRejectionReason(reason);
|
||||
|
||||
PaymentAuthorization saved = authorizationRepository.save(authorization);
|
||||
log.info("Autorização rejeitada: id={}, reason={}", authorizationId, reason);
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PaymentAuthorizationDTO findById(UUID id) {
|
||||
PaymentAuthorization authorization = authorizationRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Autorização não encontrada: " + id));
|
||||
return toDTO(authorization);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentAuthorizationDTO> findPendingApprovals(UUID approverId) {
|
||||
// Retorna todas as autorizações pendentes que o aprovador pode aprovar
|
||||
// (assumindo que o aprovador pode aprovar até o nível máximo)
|
||||
return authorizationRepository.findPendingApprovals(Integer.MAX_VALUE).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentAuthorizationDTO> findByPaymentOrderId(UUID paymentOrderId) {
|
||||
return authorizationRepository.findByPaymentOrderId(paymentOrderId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentAuthorizationDTO> findByPaymentBatchId(UUID paymentBatchId) {
|
||||
return authorizationRepository.findByPaymentBatchId(paymentBatchId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula o nível de aprovação necessário baseado no valor do pagamento.
|
||||
*/
|
||||
public Integer calculateRequiredLevel(java.math.BigDecimal amount) {
|
||||
// Valores até 100.000 XOF: 1 nível
|
||||
// Valores 100.001 - 500.000 XOF: 2 níveis
|
||||
// Valores acima de 500.000 XOF: 3 níveis
|
||||
if (amount.compareTo(new java.math.BigDecimal("100000")) <= 0) {
|
||||
return 1;
|
||||
} else if (amount.compareTo(new java.math.BigDecimal("500000")) <= 0) {
|
||||
return 2;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentAuthorizationDTO toDTO(PaymentAuthorization authorization) {
|
||||
List<ApprovalDTO> approvals = authorization.getApprovals().stream()
|
||||
.map(this::approvalToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PaymentAuthorizationDTO.builder()
|
||||
.id(authorization.getId())
|
||||
.paymentOrderId(authorization.getPaymentOrderId())
|
||||
.paymentBatchId(authorization.getPaymentBatchId())
|
||||
.requestedBy(authorization.getRequestedBy())
|
||||
.requestedAt(authorization.getRequestedAt())
|
||||
.requiredApprovalLevel(authorization.getRequiredApprovalLevel())
|
||||
.currentApprovalLevel(authorization.getCurrentApprovalLevel())
|
||||
.status(authorization.getStatus())
|
||||
.rejectionReason(authorization.getRejectionReason())
|
||||
.approvals(approvals)
|
||||
.createdAt(authorization.getCreatedAt())
|
||||
.updatedAt(authorization.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private ApprovalDTO approvalToDTO(Approval approval) {
|
||||
return ApprovalDTO.builder()
|
||||
.id(approval.getId())
|
||||
.authorizationId(approval.getAuthorization().getId())
|
||||
.level(approval.getLevel())
|
||||
.approvedBy(approval.getApprovedBy())
|
||||
.approvedAt(approval.getApprovedAt())
|
||||
.comments(approval.getComments())
|
||||
.signatureHash(approval.getSignatureHash())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentBatchDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentBatchDTO;
|
||||
import br.gov.sigefp.treasury.domain.PaymentBatch;
|
||||
import br.gov.sigefp.treasury.repository.PaymentBatchRepository;
|
||||
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 lotes de pagamento.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
public class PaymentBatchService {
|
||||
|
||||
private final PaymentBatchRepository paymentBatchRepository;
|
||||
|
||||
public PaymentBatchDTO create(CreatePaymentBatchDTO dto) {
|
||||
PaymentBatch paymentBatch = PaymentBatch.builder()
|
||||
.periodId(dto.getPeriodId())
|
||||
.ministryId(dto.getMinistryId())
|
||||
.status("CREATED")
|
||||
.build();
|
||||
|
||||
PaymentBatch saved = paymentBatchRepository.save(paymentBatch);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public PaymentBatchDTO updateStatus(UUID id, String status) {
|
||||
PaymentBatch paymentBatch = paymentBatchRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Lote de pagamento não encontrado: " + id));
|
||||
|
||||
paymentBatch.setStatus(status);
|
||||
PaymentBatch saved = paymentBatchRepository.save(paymentBatch);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PaymentBatchDTO findById(UUID id) {
|
||||
PaymentBatch paymentBatch = paymentBatchRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Lote de pagamento não encontrado: " + id));
|
||||
return toDTO(paymentBatch);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<PaymentBatchDTO> findAll(Pageable pageable) {
|
||||
return paymentBatchRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentBatchDTO> findByPeriodId(Long periodId) {
|
||||
return paymentBatchRepository.findByPeriodId(periodId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentBatchDTO> findByMinistryId(UUID ministryId) {
|
||||
return paymentBatchRepository.findByMinistryId(ministryId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentBatchDTO> findByStatus(String status) {
|
||||
return paymentBatchRepository.findByStatus(status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentBatchDTO> findByPeriodIdAndMinistryId(Long periodId, UUID ministryId) {
|
||||
return paymentBatchRepository.findByPeriodIdAndMinistryId(periodId, ministryId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentBatchDTO> findByPeriodIdAndStatus(Long periodId, String status) {
|
||||
return paymentBatchRepository.findByPeriodIdAndStatus(periodId, status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentBatchDTO> findByMinistryIdAndStatus(UUID ministryId, String status) {
|
||||
return paymentBatchRepository.findByMinistryIdAndStatus(ministryId, status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private PaymentBatchDTO toDTO(PaymentBatch paymentBatch) {
|
||||
return PaymentBatchDTO.builder()
|
||||
.id(paymentBatch.getId())
|
||||
.periodId(paymentBatch.getPeriodId())
|
||||
.ministryId(paymentBatch.getMinistryId())
|
||||
.createdAt(paymentBatch.getCreatedAt())
|
||||
.createdBy(paymentBatch.getCreatedBy())
|
||||
.status(paymentBatch.getStatus())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.rh.domain.AgentBankAccount;
|
||||
import br.gov.sigefp.rh.domain.PayrollItem;
|
||||
import br.gov.sigefp.rh.domain.PayrollRun;
|
||||
import br.gov.sigefp.rh.repository.AgentBankAccountRepository;
|
||||
import br.gov.sigefp.rh.repository.PayrollItemRepository;
|
||||
import br.gov.sigefp.rh.repository.PayrollRunRepository;
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentOrderDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentOrderDTO;
|
||||
import br.gov.sigefp.treasury.domain.PaymentBatch;
|
||||
import br.gov.sigefp.treasury.domain.PaymentOrder;
|
||||
import br.gov.sigefp.treasury.repository.PaymentBatchRepository;
|
||||
import br.gov.sigefp.treasury.repository.PaymentOrderRepository;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de ordens de pagamento.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class PaymentOrderService implements br.gov.sigefp.common.service.PaymentGenerator {
|
||||
|
||||
private final PaymentOrderRepository paymentOrderRepository;
|
||||
private final PaymentBatchRepository paymentBatchRepository;
|
||||
private final PayrollRunRepository payrollRunRepository;
|
||||
private final PayrollItemRepository payrollItemRepository;
|
||||
private final AgentBankAccountRepository agentBankAccountRepository;
|
||||
|
||||
/**
|
||||
* Gerar ordens de pagamento a partir de um payrollRun.
|
||||
* Cria uma PaymentOrder para cada agente com itens de folha (EARNING).
|
||||
*
|
||||
* @param payrollRunId ID da execução de folha (UUID)
|
||||
* @param paymentBatchId ID do lote de pagamento (opcional). Se null, cria novo
|
||||
* lote.
|
||||
*/
|
||||
@Override
|
||||
public void generateOrdersFromPayrollRun(UUID payrollRunId, UUID paymentBatchId) {
|
||||
// Implementação adaptada para void (conforme interface)
|
||||
generateOrdersInternal(payrollRunId, paymentBatchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Método interno que retorna a lista (para uso local ou testes)
|
||||
*/
|
||||
public List<PaymentOrderDTO> generateOrdersInternal(UUID payrollRunId, UUID paymentBatchId) {
|
||||
PayrollRun payrollRun = payrollRunRepository.findById(payrollRunId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Execução de folha não encontrada: " + payrollRunId));
|
||||
|
||||
if (!"COMPLETED".equals(payrollRun.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Apenas execuções de folha com status COMPLETED podem gerar ordens de pagamento",
|
||||
"INVALID_RUN_STATUS", org.springframework.http.HttpStatus.PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
PaymentBatch paymentBatch;
|
||||
if (paymentBatchId != null) {
|
||||
paymentBatch = paymentBatchRepository.findById(paymentBatchId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Lote de pagamento não encontrado: " + paymentBatchId));
|
||||
} else {
|
||||
// Auto-create PaymentBatch
|
||||
String batchRef = String.format("FOLHA-%d/%d", payrollRun.getPeriod().getMonth(),
|
||||
payrollRun.getPeriod().getFiscalYear());
|
||||
paymentBatch = PaymentBatch.builder()
|
||||
.reference(batchRef)
|
||||
.description("Folha de Pagamento Automática - " + payrollRun.getOrgUnit()) // Simplificado
|
||||
.status("PENDING")
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
paymentBatch = paymentBatchRepository.save(paymentBatch);
|
||||
log.info("Lote de pagamento criado automaticamente: {}", paymentBatch.getId());
|
||||
}
|
||||
|
||||
List<PayrollItem> items = payrollItemRepository.findByPayrollRunId(payrollRunId);
|
||||
|
||||
// Filtrar apenas itens do tipo EARNING (proventos)
|
||||
List<PayrollItem> earningItems = items.stream()
|
||||
.filter(item -> "EARNING".equals(item.getLineType()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (earningItems.isEmpty()) {
|
||||
log.warn("Nenhum item de provento encontrado na execução de folha: payrollRunId={}",
|
||||
payrollRunId);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// Agrupar itens por agente e calcular totais
|
||||
Map<UUID, List<PayrollItem>> itemsByAgent = earningItems.stream()
|
||||
.collect(Collectors.groupingBy(PayrollItem::getAgent));
|
||||
|
||||
List<PaymentOrderDTO> createdOrders = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<UUID, List<PayrollItem>> entry : itemsByAgent.entrySet()) {
|
||||
UUID agentId = entry.getKey();
|
||||
List<PayrollItem> agentItems = entry.getValue();
|
||||
|
||||
// Calcular valores brutos e líquidos
|
||||
BigDecimal grossAmount = agentItems.stream()
|
||||
.map(item -> item.getTotalAmount() != null ? item.getTotalAmount()
|
||||
: BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// Calcular descontos (itens do tipo DEDUCTION)
|
||||
List<PayrollItem> deductionItems = items.stream()
|
||||
.filter(item -> agentId.equals(item.getAgent()))
|
||||
.filter(item -> "DEDUCTION".equals(item.getLineType()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
BigDecimal totalDeductions = deductionItems.stream()
|
||||
.map(item -> item.getTotalAmount() != null ? item.getTotalAmount()
|
||||
: BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// Calcular Imposto Retido (Deduções com Classificação Econômica Iniciada em "7"
|
||||
// - Receita)
|
||||
BigDecimal taxAmount = deductionItems.stream()
|
||||
.filter(item -> item.getDeductionType() != null &&
|
||||
item.getDeductionType().getEconomicClassCode() != null &&
|
||||
item.getDeductionType().getEconomicClassCode().startsWith("7"))
|
||||
.map(item -> item.getTotalAmount() != null ? item.getTotalAmount()
|
||||
: BigDecimal.ZERO)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal netAmount = grossAmount.subtract(totalDeductions);
|
||||
|
||||
// Buscar conta bancária principal do agente
|
||||
AgentBankAccount primaryAccount = agentBankAccountRepository
|
||||
.findByAgentIdAndIsPrimaryTrue(agentId)
|
||||
.orElse(null);
|
||||
|
||||
if (primaryAccount == null) {
|
||||
log.warn("Agente sem conta bancária principal: agentId={}, payrollRunId={}", agentId,
|
||||
payrollRunId);
|
||||
// Continuar sem conta bancária - pode ser preenchida depois
|
||||
}
|
||||
|
||||
// Usar UUID nativo
|
||||
UUID payrollRunIdVal = payrollRunId;
|
||||
|
||||
// Buscar budgetLineId do primeiro item do agente (assumindo que todos os itens
|
||||
// do agente
|
||||
// usam a mesma linha orçamentária, ou pegar a primeira encontrada)
|
||||
UUID budgetLineId = agentItems.stream()
|
||||
.filter(item -> item.getBudgetLine() != null)
|
||||
.map(PayrollItem::getBudgetLine)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
// Logica de Integridade Fiscal (Tax Retention)
|
||||
String retentionType = taxAmount.compareTo(BigDecimal.ZERO) > 0 ? "AUTOMATIC_RETENTION"
|
||||
: "NONE";
|
||||
// TODO: Buscar conta de arrecadação DGI via parâmetro
|
||||
UUID taxCollectionAccount = null;
|
||||
|
||||
// Criar ordem de pagamento
|
||||
PaymentOrder paymentOrder = PaymentOrder.builder()
|
||||
.paymentBatch(paymentBatch)
|
||||
.payrollRunId(payrollRunIdVal)
|
||||
.agentId(agentId)
|
||||
.bankAccountId(primaryAccount != null ? primaryAccount.getId() : null)
|
||||
.budgetLineId(budgetLineId)
|
||||
.grossAmount(grossAmount)
|
||||
.netAmount(netAmount)
|
||||
.taxAmount(taxAmount)
|
||||
.taxRetentionType(retentionType)
|
||||
.taxCollectionAccountId(taxCollectionAccount)
|
||||
.status("CREATED")
|
||||
.build();
|
||||
|
||||
PaymentOrder saved = paymentOrderRepository.save(paymentOrder);
|
||||
createdOrders.add(toDTO(saved));
|
||||
|
||||
log.info("Ordem criada com Integridade Fiscal: orderId={}, net={}, tax={}",
|
||||
saved.getId(), netAmount, taxAmount);
|
||||
}
|
||||
|
||||
log.info("Total de ordens de pagamento criadas: {} para payrollRunId={}",
|
||||
createdOrders.size(), payrollRunId);
|
||||
|
||||
// Atualizar totais do lote
|
||||
// (Opcional, mas boa prática)
|
||||
|
||||
return createdOrders;
|
||||
}
|
||||
|
||||
public PaymentOrderDTO create(CreatePaymentOrderDTO dto) {
|
||||
PaymentBatch paymentBatch = paymentBatchRepository.findById(dto.getPaymentBatchId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Lote de pagamento não encontrado: " + dto.getPaymentBatchId()));
|
||||
|
||||
PaymentOrder paymentOrder = PaymentOrder.builder()
|
||||
.paymentBatch(paymentBatch)
|
||||
.payrollRunId(dto.getPayrollRunId())
|
||||
.agentId(dto.getAgentId())
|
||||
.bankAccountId(dto.getBankAccountId())
|
||||
.grossAmount(dto.getGrossAmount())
|
||||
.netAmount(dto.getNetAmount())
|
||||
.taxAmount(dto.getTaxAmount() != null ? dto.getTaxAmount() : BigDecimal.ZERO)
|
||||
.taxRetentionType(dto.getTaxRetentionType())
|
||||
.taxCollectionAccountId(dto.getTaxCollectionAccountId())
|
||||
.status("CREATED")
|
||||
.build();
|
||||
|
||||
PaymentOrder saved = paymentOrderRepository.save(paymentOrder);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
public PaymentOrderDTO updateStatus(UUID id, String status) {
|
||||
PaymentOrder paymentOrder = paymentOrderRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Ordem de pagamento não encontrada: " + id));
|
||||
|
||||
paymentOrder.setStatus(status);
|
||||
PaymentOrder saved = paymentOrderRepository.save(paymentOrder);
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PaymentOrderDTO findById(UUID id) {
|
||||
PaymentOrder paymentOrder = paymentOrderRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Ordem de pagamento não encontrada: " + id));
|
||||
return toDTO(paymentOrder);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<PaymentOrderDTO> findAll(Pageable pageable) {
|
||||
return paymentOrderRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentOrderDTO> findByPaymentBatchId(UUID batchId) {
|
||||
return paymentOrderRepository.findByPaymentBatchId(batchId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentOrderDTO> findByStatus(String status) {
|
||||
return paymentOrderRepository.findByStatus(status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PaymentOrderDTO> findByPaymentBatchIdAndStatus(UUID batchId, String status) {
|
||||
return paymentOrderRepository.findByPaymentBatchIdAndStatus(batchId, status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private PaymentOrderDTO toDTO(PaymentOrder paymentOrder) {
|
||||
return PaymentOrderDTO.builder()
|
||||
.id(paymentOrder.getId())
|
||||
.paymentBatchId(paymentOrder.getPaymentBatch().getId())
|
||||
.payrollRunId(paymentOrder.getPayrollRunId())
|
||||
.agentId(paymentOrder.getAgentId())
|
||||
.bankAccountId(paymentOrder.getBankAccountId())
|
||||
.budgetLineId(paymentOrder.getBudgetLineId())
|
||||
.grossAmount(paymentOrder.getGrossAmount())
|
||||
.netAmount(paymentOrder.getNetAmount())
|
||||
.taxAmount(paymentOrder.getTaxAmount())
|
||||
.taxRetentionType(paymentOrder.getTaxRetentionType())
|
||||
.taxCollectionAccountId(paymentOrder.getTaxCollectionAccountId())
|
||||
.status(paymentOrder.getStatus())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntry;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
import br.gov.sigefp.treasury.repository.TreasuryEntryRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Serviço para nivelamento automático (sweeping) de contas de trânsito.
|
||||
* Conforme Master Plan - Module 3: Unified Account Structure (CUT).
|
||||
*
|
||||
* Regra de Ouro UEMOA: Se o saldo na Conta de Trânsito > 0 no fim do dia,
|
||||
* o sistema deve transferir automaticamente para a CUT no BCEAO.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class SweepingService {
|
||||
|
||||
private final CashAccountRepository cashAccountRepository;
|
||||
private final TreasuryEntryRepository treasuryEntryRepository;
|
||||
private final CashAccountService cashAccountService;
|
||||
|
||||
/**
|
||||
* Job agendado para executar nivelamento diário às 17:00.
|
||||
* Conforme Master Plan: @Scheduled(cron = "0 0 17 * * ?")
|
||||
*/
|
||||
@Scheduled(cron = "0 0 17 * * ?") // 5 PM Daily
|
||||
public void performDailySweeping() {
|
||||
log.info("Iniciando nivelamento diário de contas de trânsito...");
|
||||
|
||||
// Encontrar todas as contas de trânsito com saldo > 0
|
||||
List<CashAccount> transitAccounts = cashAccountRepository.findAll().stream()
|
||||
.filter(account -> "TRANSIT".equals(account.getCategory()))
|
||||
.filter(account -> account.getCurrentBalance().compareTo(BigDecimal.ZERO) > 0)
|
||||
.filter(CashAccount::getIsActive)
|
||||
.toList();
|
||||
|
||||
if (transitAccounts.isEmpty()) {
|
||||
log.info("Nenhuma conta de trânsito com saldo encontrada para nivelamento");
|
||||
return;
|
||||
}
|
||||
|
||||
int sweptCount = 0;
|
||||
BigDecimal totalSwept = BigDecimal.ZERO;
|
||||
|
||||
for (CashAccount transitAccount : transitAccounts) {
|
||||
try {
|
||||
BigDecimal balance = transitAccount.getCurrentBalance();
|
||||
|
||||
// Verificar se tem conta pai (CUT)
|
||||
if (transitAccount.getParentId() == null) {
|
||||
log.warn("Conta de trânsito sem conta pai (CUT): accountId={}, code={}",
|
||||
transitAccount.getId(), transitAccount.getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
CashAccount parentAccount = cashAccountRepository.findById(transitAccount.getParentId())
|
||||
.orElse(null);
|
||||
|
||||
if (parentAccount == null) {
|
||||
log.warn("Conta pai (CUT) não encontrada: parentId={}, transitAccount={}",
|
||||
transitAccount.getParentId(), transitAccount.getCode());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Realizar transferência (sweeping)
|
||||
sweepAccount(transitAccount, parentAccount, balance);
|
||||
|
||||
sweptCount++;
|
||||
totalSwept = totalSwept.add(balance);
|
||||
|
||||
log.info("Conta nivelada: transitAccount={}, balance={}, parentAccount={}",
|
||||
transitAccount.getCode(), balance, parentAccount.getCode());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erro ao nivelar conta de trânsito: accountId={}, code={}, error={}",
|
||||
transitAccount.getId(), transitAccount.getCode(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Nivelamento diário concluído: {} contas niveladas, total transferido: {}",
|
||||
sweptCount, totalSwept);
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza o nivelamento de uma conta de trânsito para a conta pai (CUT).
|
||||
*/
|
||||
public void sweepAccount(CashAccount transitAccount, CashAccount parentAccount, BigDecimal amount) {
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("Valor do nivelamento deve ser positivo");
|
||||
}
|
||||
|
||||
if (transitAccount.getCurrentBalance().subtract(amount).compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Saldo insuficiente na conta de trânsito. Disponível: %s, Solicitado: %s",
|
||||
transitAccount.getCurrentBalance(), amount));
|
||||
}
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
// Criar entrada de tesouraria para a conta de trânsito (saída)
|
||||
TreasuryEntry transitEntry = TreasuryEntry.builder()
|
||||
.cashAccount(transitAccount)
|
||||
.type(TreasuryEntryType.CASH_WITHDRAWAL)
|
||||
.amount(amount)
|
||||
.transactionDate(today)
|
||||
.documentReference("Nivelamento Automático - " + today)
|
||||
.description(String.format("Nivelamento automático para CUT: %s",
|
||||
parentAccount.getCode()))
|
||||
.status("EXECUTED")
|
||||
.build();
|
||||
|
||||
// Criar entrada de tesouraria para a conta pai (entrada)
|
||||
TreasuryEntry parentEntry = TreasuryEntry.builder()
|
||||
.cashAccount(parentAccount)
|
||||
.type(TreasuryEntryType.CASH_DEPOSIT)
|
||||
.amount(amount)
|
||||
.transactionDate(today)
|
||||
.documentReference("Nivelamento Automático - " + today)
|
||||
.description(String.format("Nivelamento recebido de: %s", transitAccount.getCode()))
|
||||
.status("EXECUTED")
|
||||
.build();
|
||||
|
||||
treasuryEntryRepository.save(transitEntry);
|
||||
treasuryEntryRepository.save(parentEntry);
|
||||
|
||||
// Atualizar saldos
|
||||
cashAccountService.updateBalance(transitAccount.getId(), amount, "OUTFLOW");
|
||||
cashAccountService.updateBalance(parentAccount.getId(), amount, "INFLOW");
|
||||
|
||||
log.info("Nivelamento executado: {} transferido de {} para {}",
|
||||
amount, transitAccount.getCode(), parentAccount.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa nivelamento manual de uma conta específica.
|
||||
*/
|
||||
public void sweepAccountManually(UUID transitAccountId) {
|
||||
CashAccount transitAccount = cashAccountRepository.findById(transitAccountId)
|
||||
.orElseThrow(() -> new br.gov.sigefp.common.exception.ResourceNotFoundException(
|
||||
"Conta de trânsito não encontrada: " + transitAccountId));
|
||||
|
||||
if (!"TRANSIT".equals(transitAccount.getCategory())) {
|
||||
throw new br.gov.sigefp.common.exception.BusinessException(
|
||||
"A conta especificada não é uma conta de trânsito",
|
||||
"INVALID_ACCOUNT_TYPE",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
BigDecimal balance = transitAccount.getCurrentBalance();
|
||||
|
||||
if (balance.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
log.info("Conta de trânsito sem saldo para nivelar: accountId={}", transitAccountId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (transitAccount.getParentId() == null) {
|
||||
throw new br.gov.sigefp.common.exception.BusinessException(
|
||||
"Conta de trânsito sem conta pai (CUT) configurada",
|
||||
"MISSING_PARENT_ACCOUNT",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
CashAccount parentAccount = cashAccountRepository.findById(transitAccount.getParentId())
|
||||
.orElseThrow(() -> new br.gov.sigefp.common.exception.ResourceNotFoundException(
|
||||
"Conta pai (CUT) não encontrada: " + transitAccount.getParentId()));
|
||||
|
||||
sweepAccount(transitAccount, parentAccount, balance);
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateTreasuryEntryDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.TreasuryEntryDTO;
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntry;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
import br.gov.sigefp.treasury.repository.TreasuryEntryRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de entradas de tesouraria.
|
||||
* Similar a BudgetEntryService no módulo de Orçamento.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class TreasuryEntryService {
|
||||
|
||||
private final TreasuryEntryRepository treasuryEntryRepository;
|
||||
private final CashAccountRepository cashAccountRepository;
|
||||
private final CashAccountService cashAccountService;
|
||||
|
||||
public TreasuryEntryDTO create(CreateTreasuryEntryDTO dto) {
|
||||
// Validar conta de caixa
|
||||
CashAccount cashAccount = cashAccountRepository.findById(dto.getCashAccountId())
|
||||
.orElseThrow(() -> new ResourceNotFoundException(
|
||||
"Conta de caixa não encontrada: " + dto.getCashAccountId()));
|
||||
|
||||
// Validar disponibilidade de caixa para saídas
|
||||
if (isOutflowType(dto.getType()) && dto.getAmount() != null) {
|
||||
BigDecimal available = cashAccountService.getAvailableBalance(dto.getCashAccountId());
|
||||
if (dto.getAmount().compareTo(available) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Saldo disponível insuficiente. Disponível: %s, Solicitado: %s",
|
||||
available, dto.getAmount()),
|
||||
"INSUFFICIENT_CASH",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
TreasuryEntry entry = TreasuryEntry.builder()
|
||||
.cashAccount(cashAccount)
|
||||
.type(dto.getType())
|
||||
.amount(dto.getAmount())
|
||||
.transactionDate(dto.getTransactionDate())
|
||||
.documentReference(dto.getDocumentReference())
|
||||
.description(dto.getDescription())
|
||||
.status(dto.getStatus() != null ? dto.getStatus() : "DRAFT")
|
||||
.approvalLevel(dto.getApprovalLevel())
|
||||
.paymentOrderId(dto.getPaymentOrderId())
|
||||
.paymentBatchId(dto.getPaymentBatchId())
|
||||
.budgetLineId(dto.getBudgetLineId())
|
||||
.build();
|
||||
|
||||
TreasuryEntry saved = treasuryEntryRepository.save(entry);
|
||||
|
||||
// Atualizar saldo da conta se necessário
|
||||
updateCashAccountBalance(saved);
|
||||
|
||||
log.info("Entrada de tesouraria criada: id={}, type={}, amount={}",
|
||||
saved.getId(), saved.getType(), saved.getAmount());
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public TreasuryEntryDTO findById(UUID id) {
|
||||
TreasuryEntry entry = treasuryEntryRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Entrada não encontrada: " + id));
|
||||
return toDTO(entry);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<TreasuryEntryDTO> findByCashAccountId(UUID cashAccountId, Pageable pageable) {
|
||||
return treasuryEntryRepository.findByCashAccountId(cashAccountId, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<TreasuryEntryDTO> findByType(TreasuryEntryType type, Pageable pageable) {
|
||||
return treasuryEntryRepository.findByType(type, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<TreasuryEntryDTO> findByStatus(String status, Pageable pageable) {
|
||||
return treasuryEntryRepository.findByStatus(status, pageable)
|
||||
.map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal calculateAvailableBalance(UUID cashAccountId) {
|
||||
return cashAccountService.getAvailableBalance(cashAccountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza o saldo da conta de caixa baseado no tipo de entrada.
|
||||
*/
|
||||
private void updateCashAccountBalance(TreasuryEntry entry) {
|
||||
if (entry.getStatus().equals("EXECUTED")) {
|
||||
if (isOutflowType(entry.getType())) {
|
||||
cashAccountService.updateBalance(entry.getCashAccount().getId(), entry.getAmount(), "OUTFLOW");
|
||||
} else if (isInflowType(entry.getType())) {
|
||||
cashAccountService.updateBalance(entry.getCashAccount().getId(), entry.getAmount(), "INFLOW");
|
||||
}
|
||||
} else if (entry.getStatus().equals("APPROVED") && isOutflowType(entry.getType())) {
|
||||
// Comprometer saldo quando aprovado
|
||||
cashAccountService.commitBalance(entry.getCashAccount().getId(), entry.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isOutflowType(TreasuryEntryType type) {
|
||||
return type == TreasuryEntryType.PAYMENT_EXECUTION ||
|
||||
type == TreasuryEntryType.BATCH_EXECUTION ||
|
||||
type == TreasuryEntryType.CASH_WITHDRAWAL ||
|
||||
type == TreasuryEntryType.PAYMENT_CANCELLATION;
|
||||
}
|
||||
|
||||
private boolean isInflowType(TreasuryEntryType type) {
|
||||
return type == TreasuryEntryType.CASH_DEPOSIT ||
|
||||
type == TreasuryEntryType.BANK_AVAILABILITY ||
|
||||
type == TreasuryEntryType.CASH_AVAILABILITY;
|
||||
}
|
||||
|
||||
private TreasuryEntryDTO toDTO(TreasuryEntry entry) {
|
||||
return TreasuryEntryDTO.builder()
|
||||
.id(entry.getId())
|
||||
.type(entry.getType())
|
||||
.amount(entry.getAmount())
|
||||
.transactionDate(entry.getTransactionDate())
|
||||
.documentReference(entry.getDocumentReference())
|
||||
.description(entry.getDescription())
|
||||
.status(entry.getStatus())
|
||||
.approvalLevel(entry.getApprovalLevel())
|
||||
.approvedBy(entry.getApprovedBy())
|
||||
.approvedAt(entry.getApprovedAt())
|
||||
.cashAccountId(entry.getCashAccount().getId())
|
||||
.paymentOrderId(entry.getPaymentOrderId())
|
||||
.paymentBatchId(entry.getPaymentBatchId())
|
||||
.budgetLineId(entry.getBudgetLineId())
|
||||
.createdAt(entry.getCreatedAt())
|
||||
.updatedAt(entry.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.budget.integration.BudgetIntegrationService;
|
||||
import br.gov.sigefp.rh.domain.PayrollItem;
|
||||
import br.gov.sigefp.rh.domain.PayrollRun;
|
||||
import br.gov.sigefp.rh.repository.PayrollItemRepository;
|
||||
import br.gov.sigefp.rh.repository.PayrollRunRepository;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateTreasuryPaymentDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.TreasuryPaymentDTO;
|
||||
import br.gov.sigefp.treasury.domain.PaymentOrder;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryPayment;
|
||||
import br.gov.sigefp.treasury.repository.PaymentOrderRepository;
|
||||
import br.gov.sigefp.treasury.repository.TreasuryPaymentRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de pagamentos efetivados pela tesouraria.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class TreasuryPaymentService {
|
||||
|
||||
private final TreasuryPaymentRepository treasuryPaymentRepository;
|
||||
private final PaymentOrderRepository paymentOrderRepository;
|
||||
private final BudgetIntegrationService budgetIntegrationService;
|
||||
|
||||
/**
|
||||
* Registrar confirmação de pagamento (paidAt, transactionRef, status, message).
|
||||
*/
|
||||
public TreasuryPaymentDTO registerPayment(CreateTreasuryPaymentDTO dto) {
|
||||
PaymentOrder paymentOrder = paymentOrderRepository.findById(dto.getPaymentOrderId())
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Ordem de pagamento não encontrada: " + dto.getPaymentOrderId()));
|
||||
|
||||
// 1. Perna Líquida (Vendor)
|
||||
TreasuryPayment netPayment = TreasuryPayment.builder()
|
||||
.paymentOrder(paymentOrder)
|
||||
.paidAt(dto.getPaidAt() != null ? dto.getPaidAt() : Instant.now())
|
||||
.transactionRef(dto.getTransactionRef())
|
||||
.status(dto.getStatus())
|
||||
.message(dto.getMessage() != null ? dto.getMessage() : "Pagamento Líquido ao Beneficiário")
|
||||
.build();
|
||||
|
||||
TreasuryPayment savedNet = treasuryPaymentRepository.save(netPayment);
|
||||
log.info("Pagamento Líquido registrado: id={}, amount={}", savedNet.getId(), paymentOrder.getNetAmount());
|
||||
|
||||
// 2. Perna Fiscal (Integridade RN03) - Apenas se houver imposto retido e status
|
||||
// for PAID
|
||||
if ("PAID".equals(dto.getStatus()) && paymentOrder.getTaxAmount() != null &&
|
||||
paymentOrder.getTaxAmount().compareTo(java.math.BigDecimal.ZERO) > 0) {
|
||||
|
||||
TreasuryPayment taxPayment = TreasuryPayment.builder()
|
||||
.paymentOrder(paymentOrder)
|
||||
.paidAt(dto.getPaidAt() != null ? dto.getPaidAt() : Instant.now())
|
||||
.transactionRef(dto.getTransactionRef() + "-TAX")
|
||||
.status("PAID")
|
||||
.message("Retenção na Fonte: " + paymentOrder.getTaxRetentionType())
|
||||
.build();
|
||||
|
||||
TreasuryPayment savedTax = treasuryPaymentRepository.save(taxPayment);
|
||||
log.info("Retenção Fiscal registrada: id={}, amount={}, targetAccount={}",
|
||||
savedTax.getId(), paymentOrder.getTaxAmount(), paymentOrder.getTaxCollectionAccountId());
|
||||
}
|
||||
|
||||
// Atualizar status da ordem de pagamento se o pagamento foi confirmado
|
||||
if ("PAID".equals(dto.getStatus())) {
|
||||
paymentOrder.setStatus("PAID");
|
||||
paymentOrderRepository.save(paymentOrder);
|
||||
|
||||
// Criar execução orçamentária do tipo PAYMENT
|
||||
try {
|
||||
if (paymentOrder.getBudgetLineId() != null && paymentOrder.getGrossAmount() != null) {
|
||||
// Calcular periodId a partir da data do pagamento
|
||||
LocalDate paymentDate = savedNet.getPaidAt().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
||||
YearMonth paymentPeriod = YearMonth.from(paymentDate);
|
||||
Long periodId = BudgetIntegrationService.toPeriodId(
|
||||
paymentPeriod.getYear(),
|
||||
paymentPeriod.getMonthValue());
|
||||
|
||||
// Criar execução orçamentária do tipo PAYMENT
|
||||
// IMPORTANTE: Execução Orçamentária deve refletir o VALOR BRUTO (Despesa Total)
|
||||
budgetIntegrationService.createPaymentFromTreasury(
|
||||
paymentOrder.getBudgetLineId(),
|
||||
periodId,
|
||||
paymentOrder.getGrossAmount(), // Alterado de netAmount para grossAmount
|
||||
savedNet.getId());
|
||||
|
||||
log.info(
|
||||
"Execução orçamentária (PAYMENT) criada a partir de pagamento: paymentOrderId={}, budgetLineId={}, grossAmount={}",
|
||||
paymentOrder.getId(), paymentOrder.getBudgetLineId(), paymentOrder.getGrossAmount());
|
||||
} else {
|
||||
log.warn(
|
||||
"Não foi possível criar execução orçamentária: paymentOrderId={}, budgetLineId={}, amount={}",
|
||||
paymentOrder.getId(), paymentOrder.getBudgetLineId(), paymentOrder.getGrossAmount());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erro ao criar execução orçamentária a partir de pagamento: {}", e.getMessage(), e);
|
||||
// Não falha o registro do pagamento se a execução orçamentária falhar
|
||||
}
|
||||
} else if ("REJECTED".equals(dto.getStatus())) {
|
||||
paymentOrder.setStatus("REJECTED");
|
||||
paymentOrderRepository.save(paymentOrder);
|
||||
}
|
||||
|
||||
return toDTO(savedNet);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public TreasuryPaymentDTO findById(UUID id) {
|
||||
TreasuryPayment treasuryPayment = treasuryPaymentRepository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Pagamento não encontrado: " + id));
|
||||
return toDTO(treasuryPayment);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<TreasuryPaymentDTO> findAll(Pageable pageable) {
|
||||
return treasuryPaymentRepository.findAll(pageable).map(this::toDTO);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<TreasuryPaymentDTO> findByPaymentOrderId(UUID paymentOrderId) {
|
||||
return treasuryPaymentRepository.findByPaymentOrderId(paymentOrderId).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<TreasuryPaymentDTO> findByStatus(String status) {
|
||||
return treasuryPaymentRepository.findByStatus(status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<TreasuryPaymentDTO> findByPaymentOrderIdAndStatus(UUID paymentOrderId, String status) {
|
||||
return treasuryPaymentRepository.findByPaymentOrderIdAndStatus(paymentOrderId, status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private TreasuryPaymentDTO toDTO(TreasuryPayment treasuryPayment) {
|
||||
return TreasuryPaymentDTO.builder()
|
||||
.id(treasuryPayment.getId())
|
||||
.paymentOrderId(treasuryPayment.getPaymentOrder().getId())
|
||||
.paidAt(treasuryPayment.getPaidAt())
|
||||
.transactionRef(treasuryPayment.getTransactionRef())
|
||||
.status(treasuryPayment.getStatus())
|
||||
.message(treasuryPayment.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateTreasuryPlanDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.TreasuryPlanDTO;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryPlan;
|
||||
import br.gov.sigefp.treasury.repository.TreasuryPlanRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Serviço de aplicação para gestão de Planos de Tesouraria (PT).
|
||||
* Conforme Master Plan - Module 2: Preventive Control.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional
|
||||
@Slf4j
|
||||
public class TreasuryPlanService {
|
||||
|
||||
private final TreasuryPlanRepository treasuryPlanRepository;
|
||||
|
||||
/**
|
||||
* Cria um novo plano de tesouraria.
|
||||
*/
|
||||
public TreasuryPlanDTO createPlan(CreateTreasuryPlanDTO dto) {
|
||||
// Verificar se já existe plano para o mesmo ano/mês
|
||||
treasuryPlanRepository.findByFiscalYearAndReferenceMonth(
|
||||
dto.getFiscalYear(), dto.getReferenceMonth())
|
||||
.ifPresent(existing -> {
|
||||
throw new BusinessException(
|
||||
String.format("Já existe um plano para o ano %d, mês %d",
|
||||
dto.getFiscalYear(), dto.getReferenceMonth()),
|
||||
"DUPLICATE_PLAN",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
});
|
||||
|
||||
// Calcular datas se não fornecidas
|
||||
LocalDate startDate = dto.getStartDate();
|
||||
LocalDate endDate = dto.getEndDate();
|
||||
|
||||
if (startDate == null || endDate == null) {
|
||||
YearMonth yearMonth = YearMonth.of(dto.getFiscalYear(), dto.getReferenceMonth());
|
||||
startDate = yearMonth.atDay(1);
|
||||
endDate = yearMonth.atEndOfMonth();
|
||||
}
|
||||
|
||||
// Validar datas
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new BusinessException(
|
||||
"Data de fim deve ser posterior à data de início",
|
||||
"INVALID_DATE_RANGE",
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
TreasuryPlan plan = TreasuryPlan.builder()
|
||||
.fiscalYear(dto.getFiscalYear())
|
||||
.referenceMonth(dto.getReferenceMonth())
|
||||
.approvedCeiling(dto.getApprovedCeiling())
|
||||
.executedAmount(BigDecimal.ZERO)
|
||||
.startDate(startDate)
|
||||
.endDate(endDate)
|
||||
.status("DRAFT")
|
||||
.build();
|
||||
|
||||
TreasuryPlan saved = treasuryPlanRepository.save(plan);
|
||||
log.info("Plano de Tesouraria criado: id={}, fiscalYear={}, month={}, ceiling={}",
|
||||
saved.getId(), saved.getFiscalYear(), saved.getReferenceMonth(), saved.getApprovedCeiling());
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aprova um plano de tesouraria.
|
||||
*/
|
||||
public TreasuryPlanDTO approvePlan(UUID planId, UUID approverId) {
|
||||
TreasuryPlan plan = treasuryPlanRepository.findById(planId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Plano não encontrado: " + planId));
|
||||
|
||||
if (!"DRAFT".equals(plan.getStatus())) {
|
||||
throw new BusinessException(
|
||||
"Apenas planos em rascunho podem ser aprovados",
|
||||
"INVALID_STATUS",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
// Fechar planos anteriores sobrepostos
|
||||
List<TreasuryPlan> overlappingPlans = treasuryPlanRepository.findApprovedPlansForPeriod(
|
||||
plan.getStartDate(), plan.getEndDate());
|
||||
|
||||
for (TreasuryPlan overlapping : overlappingPlans) {
|
||||
if (overlapping.getStatus().equals("APPROVED")) {
|
||||
overlapping.setStatus("CLOSED");
|
||||
treasuryPlanRepository.save(overlapping);
|
||||
log.info("Plano sobreposto fechado: id={}", overlapping.getId());
|
||||
}
|
||||
}
|
||||
|
||||
plan.setStatus("APPROVED");
|
||||
plan.setApprovedBy(approverId);
|
||||
plan.setApprovedAt(java.time.LocalDateTime.now());
|
||||
|
||||
TreasuryPlan saved = treasuryPlanRepository.save(plan);
|
||||
log.info("Plano de Tesouraria aprovado: id={}, approvedBy={}", planId, approverId);
|
||||
|
||||
return toDTO(saved);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida disponibilidade de fundos conforme o plano ativo.
|
||||
* Conforme Master Plan 2.3: Integration Point.
|
||||
*
|
||||
* @param amount Valor a ser validado
|
||||
* @return true se há disponibilidade, false caso contrário
|
||||
* @throws BusinessException se o teto for excedido
|
||||
*/
|
||||
public boolean validateAvailability(BigDecimal amount) {
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
TreasuryPlan activePlan = treasuryPlanRepository.findActivePlanForDate(today)
|
||||
.orElse(null);
|
||||
|
||||
if (activePlan == null) {
|
||||
log.warn("Nenhum plano ativo encontrado para a data: {}", today);
|
||||
// Se não houver plano, permitir (pode ser configurável)
|
||||
return true;
|
||||
}
|
||||
|
||||
BigDecimal available = activePlan.getAvailableAmount();
|
||||
|
||||
if (amount.compareTo(available) > 0) {
|
||||
throw new BusinessException(
|
||||
String.format("Valor excede o teto disponível no Plano de Tesouraria. " +
|
||||
"Disponível: %s, Solicitado: %s, Plano: %d/%d",
|
||||
available, amount, activePlan.getFiscalYear(), activePlan.getReferenceMonth()),
|
||||
"CEILING_EXCEEDED",
|
||||
org.springframework.http.HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza o valor executado do plano (chamado após autorização de pagamento).
|
||||
*/
|
||||
public void updateExecutedAmount(UUID planId, BigDecimal amount) {
|
||||
TreasuryPlan plan = treasuryPlanRepository.findById(planId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Plano não encontrado: " + planId));
|
||||
|
||||
plan.setExecutedAmount(plan.getExecutedAmount().add(amount));
|
||||
treasuryPlanRepository.save(plan);
|
||||
|
||||
log.debug("Valor executado atualizado: planId={}, newExecutedAmount={}",
|
||||
planId, plan.getExecutedAmount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Encontra o plano ativo para uma data específica.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public TreasuryPlanDTO findActivePlanForDate(LocalDate date) {
|
||||
return treasuryPlanRepository.findActivePlanForDate(date)
|
||||
.map(this::toDTO)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public TreasuryPlanDTO findById(UUID id) {
|
||||
TreasuryPlan plan = treasuryPlanRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Plano não encontrado: " + id));
|
||||
return toDTO(plan);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<TreasuryPlanDTO> findByStatus(String status) {
|
||||
return treasuryPlanRepository.findByStatus(status).stream()
|
||||
.map(this::toDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula o valor executado para um plano específico.
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public BigDecimal calculateExecutedAmount(UUID planId) {
|
||||
TreasuryPlan plan = treasuryPlanRepository.findById(planId)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Plano não encontrado: " + planId));
|
||||
|
||||
return treasuryPlanRepository.calculateExecutedAmountForPeriod(
|
||||
plan.getStartDate(), plan.getEndDate());
|
||||
}
|
||||
|
||||
private TreasuryPlanDTO toDTO(TreasuryPlan plan) {
|
||||
return TreasuryPlanDTO.builder()
|
||||
.id(plan.getId())
|
||||
.fiscalYear(plan.getFiscalYear())
|
||||
.referenceMonth(plan.getReferenceMonth())
|
||||
.status(plan.getStatus())
|
||||
.approvedCeiling(plan.getApprovedCeiling())
|
||||
.executedAmount(plan.getExecutedAmount())
|
||||
.availableAmount(plan.getAvailableAmount())
|
||||
.startDate(plan.getStartDate())
|
||||
.endDate(plan.getEndDate())
|
||||
.approvedBy(plan.getApprovedBy())
|
||||
.approvedAt(plan.getApprovedAt())
|
||||
.createdAt(plan.getCreatedAt())
|
||||
.updatedAt(plan.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package br.gov.sigefp.treasury;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableJpaRepositories(basePackages = "br.gov.sigefp")
|
||||
@EntityScan(basePackages = "br.gov.sigefp")
|
||||
@ComponentScan(basePackages = "br.gov.sigefp")
|
||||
public class TestTreasuryApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(TestTreasuryApplication.class, args);
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.common.exception.ResourceNotFoundException;
|
||||
import br.gov.sigefp.treasury.api.dto.CashAccountDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.CreateCashAccountDTO;
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
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 CashAccountServiceTest {
|
||||
|
||||
@Mock
|
||||
private CashAccountRepository cashAccountRepository;
|
||||
|
||||
@InjectMocks
|
||||
private CashAccountService cashAccountService;
|
||||
|
||||
private CashAccount cashAccount;
|
||||
private CreateCashAccountDTO createDTO;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
cashAccount = CashAccount.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.code("CAIXA-001")
|
||||
.name("Caixa Principal")
|
||||
.type("CASH")
|
||||
.currentBalance(new BigDecimal("1000.00"))
|
||||
.availableBalance(new BigDecimal("1000.00"))
|
||||
.isActive(true)
|
||||
.build();
|
||||
|
||||
createDTO = new CreateCashAccountDTO();
|
||||
createDTO.setCode("CAIXA-001");
|
||||
createDTO.setName("Caixa Principal");
|
||||
createDTO.setType("CASH");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should create cash account successfully")
|
||||
void create_Success() {
|
||||
when(cashAccountRepository.findByCode(createDTO.getCode())).thenReturn(Optional.empty());
|
||||
when(cashAccountRepository.save(any(CashAccount.class))).thenReturn(cashAccount);
|
||||
|
||||
CashAccountDTO result = cashAccountService.create(createDTO);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(cashAccount.getCode(), result.getCode());
|
||||
verify(cashAccountRepository).save(any(CashAccount.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when creating duplicate code")
|
||||
void create_DuplicateCode() {
|
||||
when(cashAccountRepository.findByCode(createDTO.getCode())).thenReturn(Optional.of(cashAccount));
|
||||
|
||||
assertThrows(BusinessException.class, () -> cashAccountService.create(createDTO));
|
||||
verify(cashAccountRepository, never()).save(any(CashAccount.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should update balance correctly on INFLOW")
|
||||
void updateBalance_Inflow() {
|
||||
when(cashAccountRepository.findById(cashAccount.getId())).thenReturn(Optional.of(cashAccount));
|
||||
when(cashAccountRepository.save(any(CashAccount.class))).thenReturn(cashAccount);
|
||||
|
||||
BigDecimal amount = new BigDecimal("500.00");
|
||||
cashAccountService.updateBalance(cashAccount.getId(), amount, "INFLOW");
|
||||
|
||||
assertEquals(new BigDecimal("1500.00"), cashAccount.getCurrentBalance());
|
||||
assertEquals(new BigDecimal("1500.00"), cashAccount.getAvailableBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should update balance correctly on OUTFLOW")
|
||||
void updateBalance_Outflow() {
|
||||
when(cashAccountRepository.findById(cashAccount.getId())).thenReturn(Optional.of(cashAccount));
|
||||
when(cashAccountRepository.save(any(CashAccount.class))).thenReturn(cashAccount);
|
||||
|
||||
BigDecimal amount = new BigDecimal("200.00");
|
||||
cashAccountService.updateBalance(cashAccount.getId(), amount, "OUTFLOW");
|
||||
|
||||
assertEquals(new BigDecimal("800.00"), cashAccount.getCurrentBalance());
|
||||
assertEquals(new BigDecimal("800.00"), cashAccount.getAvailableBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should commit balance successfully")
|
||||
void commitBalance_Success() {
|
||||
when(cashAccountRepository.findById(cashAccount.getId())).thenReturn(Optional.of(cashAccount));
|
||||
when(cashAccountRepository.save(any(CashAccount.class))).thenReturn(cashAccount);
|
||||
|
||||
BigDecimal amount = new BigDecimal("300.00");
|
||||
cashAccountService.commitBalance(cashAccount.getId(), amount);
|
||||
|
||||
assertEquals(new BigDecimal("1000.00"), cashAccount.getCurrentBalance()); // Current unchangd
|
||||
assertEquals(new BigDecimal("700.00"), cashAccount.getAvailableBalance()); // Available reduced
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when committing more than available")
|
||||
void commitBalance_InsufficientFunds() {
|
||||
when(cashAccountRepository.findById(cashAccount.getId())).thenReturn(Optional.of(cashAccount));
|
||||
|
||||
BigDecimal amount = new BigDecimal("1500.00"); // More than 1000
|
||||
|
||||
assertThrows(BusinessException.class, () -> cashAccountService.commitBalance(cashAccount.getId(), amount));
|
||||
|
||||
assertEquals(new BigDecimal("1000.00"), cashAccount.getAvailableBalance()); // Should not have changed
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should release balance correctly")
|
||||
void releaseBalance_Success() {
|
||||
when(cashAccountRepository.findById(cashAccount.getId())).thenReturn(Optional.of(cashAccount));
|
||||
when(cashAccountRepository.save(any(CashAccount.class))).thenReturn(cashAccount);
|
||||
|
||||
// Assume some blocked balance, available is 1000
|
||||
BigDecimal amount = new BigDecimal("200.00");
|
||||
cashAccountService.releaseBalance(cashAccount.getId(), amount);
|
||||
|
||||
assertEquals(new BigDecimal("1200.00"), cashAccount.getAvailableBalance());
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
import br.gov.sigefp.treasury.api.dto.CreatePaymentAuthorizationDTO;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentAuthorizationDTO;
|
||||
import br.gov.sigefp.treasury.domain.Approval;
|
||||
import br.gov.sigefp.treasury.domain.PaymentAuthorization;
|
||||
import br.gov.sigefp.treasury.repository.ApprovalRepository;
|
||||
import br.gov.sigefp.treasury.repository.PaymentAuthorizationRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
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 PaymentAuthorizationServiceTest {
|
||||
|
||||
@Mock
|
||||
private PaymentAuthorizationRepository authorizationRepository;
|
||||
|
||||
@Mock
|
||||
private ApprovalRepository approvalRepository;
|
||||
|
||||
@InjectMocks
|
||||
private PaymentAuthorizationService authorizationService;
|
||||
|
||||
private PaymentAuthorization authorization;
|
||||
private CreatePaymentAuthorizationDTO createDTO;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
authorization = PaymentAuthorization.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.paymentOrderId(UUID.randomUUID())
|
||||
.requestedBy(UUID.randomUUID())
|
||||
.requestedAt(LocalDateTime.now())
|
||||
.requiredApprovalLevel(2)
|
||||
.currentApprovalLevel(1)
|
||||
.status("PENDING")
|
||||
.approvals(new ArrayList<>())
|
||||
.build();
|
||||
|
||||
createDTO = new CreatePaymentAuthorizationDTO();
|
||||
createDTO.setPaymentOrderId(authorization.getPaymentOrderId());
|
||||
createDTO.setRequestedBy(authorization.getRequestedBy());
|
||||
createDTO.setRequiredApprovalLevel(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should request authorization successfully")
|
||||
void requestAuthorization_Success() {
|
||||
when(authorizationRepository.save(any(PaymentAuthorization.class))).thenReturn(authorization);
|
||||
|
||||
PaymentAuthorizationDTO result = authorizationService.requestAuthorization(createDTO);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("PENDING", result.getStatus());
|
||||
assertEquals(1, result.getCurrentApprovalLevel());
|
||||
verify(authorizationRepository).save(any(PaymentAuthorization.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should approve authorization successfully (Partial Approval)")
|
||||
void approve_PartialSuccess() {
|
||||
// Setup: Level 1 of 2
|
||||
when(authorizationRepository.findById(authorization.getId())).thenReturn(Optional.of(authorization));
|
||||
when(authorizationRepository.save(any(PaymentAuthorization.class))).thenReturn(authorization);
|
||||
// Approval repository save is called, mock it? Not strictly needed for
|
||||
// void/save return unless captured
|
||||
|
||||
PaymentAuthorizationDTO result = authorizationService.approve(authorization.getId(), UUID.randomUUID(),
|
||||
"Looks good");
|
||||
|
||||
// Verify status changed to PARTIALLY_APPROVED and level incremented
|
||||
// Note: The service modifies the object in place, so our mock return reflects
|
||||
// that if we return the SAME object
|
||||
assertEquals("PARTIALLY_APPROVED", result.getStatus());
|
||||
assertEquals(2, result.getCurrentApprovalLevel());
|
||||
verify(approvalRepository).save(any(Approval.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should finalize approval when all levels are met")
|
||||
void approve_FinalSuccess() {
|
||||
// Setup: Level 2 of 2 (Already at level 2 manually for this test case?)
|
||||
// Or simpler: Current Level is 1, Required is 1.
|
||||
authorization.setRequiredApprovalLevel(1);
|
||||
|
||||
when(authorizationRepository.findById(authorization.getId())).thenReturn(Optional.of(authorization));
|
||||
when(authorizationRepository.save(any(PaymentAuthorization.class))).thenReturn(authorization);
|
||||
|
||||
PaymentAuthorizationDTO result = authorizationService.approve(authorization.getId(), UUID.randomUUID(),
|
||||
"Final OK");
|
||||
|
||||
assertEquals("APPROVED", result.getStatus());
|
||||
verify(approvalRepository).save(any(Approval.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject authorization successfully")
|
||||
void reject_Success() {
|
||||
when(authorizationRepository.findById(authorization.getId())).thenReturn(Optional.of(authorization));
|
||||
when(authorizationRepository.save(any(PaymentAuthorization.class))).thenReturn(authorization);
|
||||
|
||||
PaymentAuthorizationDTO result = authorizationService.reject(authorization.getId(), UUID.randomUUID(),
|
||||
"Bad Payment");
|
||||
|
||||
assertEquals("REJECTED", result.getStatus());
|
||||
assertEquals("Bad Payment", result.getRejectionReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should calculate required level correctly")
|
||||
void calculateRequiredLevel_Logic() {
|
||||
// <= 100,000 -> 1
|
||||
assertEquals(1, authorizationService.calculateRequiredLevel(new BigDecimal("50000")));
|
||||
assertEquals(1, authorizationService.calculateRequiredLevel(new BigDecimal("100000")));
|
||||
|
||||
// 100,001 - 500,000 -> 2
|
||||
assertEquals(2, authorizationService.calculateRequiredLevel(new BigDecimal("100001")));
|
||||
assertEquals(2, authorizationService.calculateRequiredLevel(new BigDecimal("500000")));
|
||||
|
||||
// > 500,000 -> 3
|
||||
assertEquals(3, authorizationService.calculateRequiredLevel(new BigDecimal("500001")));
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.rh.domain.AgentBankAccount;
|
||||
import br.gov.sigefp.rh.domain.PayrollItem;
|
||||
import br.gov.sigefp.rh.domain.PayrollPeriod;
|
||||
import br.gov.sigefp.rh.domain.PayrollRun;
|
||||
import br.gov.sigefp.rh.repository.AgentBankAccountRepository;
|
||||
import br.gov.sigefp.rh.repository.PayrollItemRepository;
|
||||
import br.gov.sigefp.rh.repository.PayrollRunRepository;
|
||||
import br.gov.sigefp.treasury.api.dto.PaymentOrderDTO;
|
||||
import br.gov.sigefp.treasury.domain.PaymentBatch;
|
||||
import br.gov.sigefp.treasury.domain.PaymentOrder;
|
||||
import br.gov.sigefp.treasury.repository.PaymentBatchRepository;
|
||||
import br.gov.sigefp.treasury.repository.PaymentOrderRepository;
|
||||
import br.gov.sigefp.common.exception.BusinessException;
|
||||
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.List;
|
||||
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 PaymentOrderServiceTest {
|
||||
|
||||
@Mock
|
||||
private PaymentOrderRepository paymentOrderRepository;
|
||||
@Mock
|
||||
private PaymentBatchRepository paymentBatchRepository;
|
||||
@Mock
|
||||
private PayrollRunRepository payrollRunRepository;
|
||||
@Mock
|
||||
private PayrollItemRepository payrollItemRepository;
|
||||
@Mock
|
||||
private AgentBankAccountRepository agentBankAccountRepository;
|
||||
|
||||
@InjectMocks
|
||||
private PaymentOrderService paymentOrderService;
|
||||
|
||||
private UUID payrollRunId;
|
||||
private UUID batchId;
|
||||
private UUID agentId;
|
||||
private PayrollRun payrollRun;
|
||||
private PaymentBatch batch;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
payrollRunId = UUID.randomUUID();
|
||||
batchId = UUID.randomUUID();
|
||||
agentId = UUID.randomUUID();
|
||||
|
||||
PayrollPeriod period = PayrollPeriod.builder().fiscalYear(2024).month(1).build();
|
||||
payrollRun = PayrollRun.builder().status("COMPLETED").period(period).build();
|
||||
payrollRun.setId(payrollRunId);
|
||||
batch = PaymentBatch.builder().build();
|
||||
batch.setId(batchId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve gerar ordens de pagamento agrupadas por agente corretamente")
|
||||
void generateOrdersFromPayrollRun_Success() {
|
||||
// Mocks
|
||||
when(payrollRunRepository.findById(payrollRunId)).thenReturn(Optional.of(payrollRun));
|
||||
when(paymentBatchRepository.findById(batchId)).thenReturn(Optional.of(batch));
|
||||
|
||||
// Itens de Folha para o mesmo Agente: 1 Earning (1000) e 1 Deduction (200)
|
||||
PayrollItem earning = PayrollItem.builder()
|
||||
.agent(agentId)
|
||||
.lineType("EARNING")
|
||||
.totalAmount(new BigDecimal("1000"))
|
||||
.build();
|
||||
|
||||
PayrollItem deduction = PayrollItem.builder()
|
||||
.agent(agentId)
|
||||
.lineType("DEDUCTION")
|
||||
.totalAmount(new BigDecimal("200"))
|
||||
.build();
|
||||
|
||||
when(payrollItemRepository.findByPayrollRunId(payrollRunId)).thenReturn(List.of(earning, deduction));
|
||||
|
||||
AgentBankAccount account = AgentBankAccount.builder().build();
|
||||
account.setId(UUID.randomUUID());
|
||||
when(agentBankAccountRepository.findByAgentIdAndIsPrimaryTrue(agentId)).thenReturn(Optional.of(account));
|
||||
|
||||
when(paymentOrderRepository.save(any(PaymentOrder.class))).thenAnswer(i -> {
|
||||
PaymentOrder po = i.getArgument(0);
|
||||
po.setId(UUID.randomUUID());
|
||||
return po;
|
||||
});
|
||||
|
||||
// Execute
|
||||
List<PaymentOrderDTO> result = paymentOrderService.generateOrdersInternal(payrollRunId, batchId);
|
||||
|
||||
// Verify
|
||||
assertEquals(1, result.size());
|
||||
PaymentOrderDTO order = result.get(0);
|
||||
assertEquals(new BigDecimal("1000"), order.getGrossAmount());
|
||||
assertEquals(new BigDecimal("800"), order.getNetAmount());
|
||||
assertEquals(agentId, order.getAgentId());
|
||||
assertEquals(payrollRunId, order.getPayrollRunId()); // Agora é UUID nativo!
|
||||
|
||||
verify(paymentOrderRepository, times(1)).save(any(PaymentOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve falhar se a folha não estiver completa")
|
||||
void generateOrders_FailsIfStatusNotCompleted() {
|
||||
payrollRun.setStatus("PENDING");
|
||||
when(payrollRunRepository.findById(payrollRunId)).thenReturn(Optional.of(payrollRun));
|
||||
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> paymentOrderService.generateOrdersInternal(payrollRunId, batchId));
|
||||
|
||||
assertTrue(exception.getMessage().contains("Apenas execuções de folha com status COMPLETED"));
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package br.gov.sigefp.treasury.service;
|
||||
|
||||
import br.gov.sigefp.treasury.api.dto.*;
|
||||
import br.gov.sigefp.treasury.domain.CashAccount;
|
||||
import br.gov.sigefp.treasury.domain.TreasuryEntryType;
|
||||
import br.gov.sigefp.treasury.repository.CashAccountRepository;
|
||||
import br.gov.sigefp.treasury.repository.PaymentBatchRepository;
|
||||
import br.gov.sigefp.treasury.repository.PaymentOrderRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
class TreasuryIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private CashAccountService cashAccountService;
|
||||
|
||||
@Autowired
|
||||
private PaymentOrderService paymentOrderService;
|
||||
|
||||
@Autowired
|
||||
private PaymentAuthorizationService authorizationService;
|
||||
|
||||
@Autowired
|
||||
private TreasuryEntryService treasuryEntryService;
|
||||
|
||||
@Autowired
|
||||
private CashAccountRepository cashAccountRepository;
|
||||
|
||||
@Autowired
|
||||
private PaymentBatchRepository paymentBatchRepository;
|
||||
|
||||
@Autowired
|
||||
private PaymentOrderRepository paymentOrderRepository;
|
||||
|
||||
private UUID cashAccountId;
|
||||
private UUID paymentBatchId;
|
||||
private UUID paymentOrderId;
|
||||
private UUID requesterId = UUID.randomUUID();
|
||||
private UUID approverId = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 1. Create Cash Account with funds
|
||||
CreateCashAccountDTO accountDTO = new CreateCashAccountDTO();
|
||||
accountDTO.setCode("TEST-TSA-" + UUID.randomUUID());
|
||||
accountDTO.setName("Conta Única de Teste");
|
||||
accountDTO.setType("BANK_ACCOUNT");
|
||||
accountDTO.setBankId(UUID.randomUUID());
|
||||
CashAccountDTO account = cashAccountService.create(accountDTO);
|
||||
cashAccountId = account.getId();
|
||||
|
||||
// Deposit funds (1,000,000 XOF)
|
||||
treasuryEntryService.create(CreateTreasuryEntryDTO.builder()
|
||||
.cashAccountId(cashAccountId)
|
||||
.type(TreasuryEntryType.CASH_DEPOSIT)
|
||||
.amount(new BigDecimal("1000000"))
|
||||
.transactionDate(LocalDate.now())
|
||||
.description("Dotação Inicial de Caixa")
|
||||
.status("EXECUTED")
|
||||
.build());
|
||||
|
||||
// 2. Create Payment Batch (Mocking simple Batch creation as Service not fully
|
||||
// mocked here)
|
||||
// Ideally use PaymentBatchService, but for speed injecting repo is fine
|
||||
// regarding this integration test scope
|
||||
// Actually lets use a "virtual" batch ID for order creation if allowed or
|
||||
// create raw entity
|
||||
// To avoid dependency hell, I'll simulate order creation directly if possible
|
||||
// or create a mock batch
|
||||
// Using PaymentOrderService.create requires a real batch in repo usually
|
||||
|
||||
br.gov.sigefp.treasury.domain.PaymentBatch batch = br.gov.sigefp.treasury.domain.PaymentBatch.builder()
|
||||
.status("OPEN")
|
||||
.build();
|
||||
paymentBatchRepository.save(batch);
|
||||
paymentBatchId = batch.getId();
|
||||
|
||||
// 3. Create Payment Order (150,000 XOF - Requires Level 2 Approval)
|
||||
CreatePaymentOrderDTO orderDTO = CreatePaymentOrderDTO.builder()
|
||||
.paymentBatchId(paymentBatchId)
|
||||
.agentId(UUID.randomUUID())
|
||||
.bankAccountId(UUID.randomUUID()) // Dummy bank account
|
||||
.grossAmount(new BigDecimal("150000"))
|
||||
.netAmount(new BigDecimal("150000"))
|
||||
.build();
|
||||
|
||||
PaymentOrderDTO order = paymentOrderService.create(orderDTO);
|
||||
paymentOrderId = order.getId();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("End-to-End: Request -> Approve -> Execute Payment")
|
||||
void testEndToEndPaymentFlow() {
|
||||
// Step 1: Request Authorization
|
||||
// 150k requires Level 1 (<=100k) or Level 2?
|
||||
// Logic: <= 100k -> 1; <= 500k -> 2. So 150k is Level 2.
|
||||
Integer requiredLevel = authorizationService.calculateRequiredLevel(new BigDecimal("150000"));
|
||||
assertEquals(2, requiredLevel, "Should require 2 levels of approval");
|
||||
|
||||
CreatePaymentAuthorizationDTO authReq = CreatePaymentAuthorizationDTO.builder()
|
||||
.paymentOrderId(paymentOrderId)
|
||||
.paymentBatchId(paymentBatchId)
|
||||
.requestedBy(requesterId)
|
||||
.requiredApprovalLevel(requiredLevel)
|
||||
.build();
|
||||
|
||||
PaymentAuthorizationDTO auth = authorizationService.requestAuthorization(authReq);
|
||||
assertEquals("PENDING", auth.getStatus());
|
||||
assertEquals(1, auth.getCurrentApprovalLevel());
|
||||
|
||||
// Step 2: Approve Level 1
|
||||
auth = authorizationService.approve(auth.getId(), approverId, "Approving Level 1");
|
||||
assertEquals("PARTIALLY_APPROVED", auth.getStatus());
|
||||
assertEquals(2, auth.getCurrentApprovalLevel());
|
||||
|
||||
// Step 3: Approve Level 2
|
||||
auth = authorizationService.approve(auth.getId(), UUID.randomUUID(), "Approving Level 2 (Final)");
|
||||
assertEquals("APPROVED", auth.getStatus());
|
||||
|
||||
// Step 4: Execute Payment (Treasury Entry Outflow)
|
||||
CreateTreasuryEntryDTO paymentEntry = CreateTreasuryEntryDTO.builder()
|
||||
.cashAccountId(cashAccountId)
|
||||
.paymentOrderId(paymentOrderId)
|
||||
.type(TreasuryEntryType.PAYMENT_EXECUTION)
|
||||
.amount(new BigDecimal("150000"))
|
||||
.transactionDate(LocalDate.now())
|
||||
.description("Pagamento Salário Teste")
|
||||
.status("EXECUTED")
|
||||
.build();
|
||||
|
||||
TreasuryEntryDTO executedEntry = treasuryEntryService.create(paymentEntry);
|
||||
assertEquals("EXECUTED", executedEntry.getStatus());
|
||||
|
||||
// Step 5: Verify Balance Impact
|
||||
// Initial: 1,000,000 -> Paid: 150,000 -> Remaining: 850,000
|
||||
BigDecimal finalBalance = cashAccountService.getCurrentBalance(cashAccountId);
|
||||
assertEquals(0, finalBalance.compareTo(new BigDecimal("850000")), "Balance should be 850,000");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
Reference in New Issue
Block a user