feat: otimização de performance e ajustes finais

This commit is contained in:
Idrissa Banora
2026-05-18 10:49:32 +00:00
commit 52a7c4f9cf
579 changed files with 156489 additions and 0 deletions
@@ -0,0 +1,81 @@
<?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-common</artifactId>
<packaging>jar</packaging>
<name>SIGEFP Common</name>
<description>Utilitários e classes compartilhadas</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<!-- Lombok para reduzir boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Excel (Apache POI) -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<!-- PDF (OpenPDF) -->
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
<version>1.3.30</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,74 @@
package br.gov.sigefp.common.api;
import br.gov.sigefp.common.api.dto.BankDTO;
import br.gov.sigefp.common.service.BankService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/**
* Controller REST para gestão de bancos.
*/
@RestController
@RequestMapping("/api/common/banks")
@RequiredArgsConstructor
public class BankController {
private final BankService bankService;
@GetMapping
public ResponseEntity<Page<BankDTO>> findAll(
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sortBy", required = false) String sortBy,
@RequestParam(name = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.ASC, "code");
Pageable pageable = PageRequest.of(page, size, sort);
Page<BankDTO> result = bankService.findAll(pageable);
return ResponseEntity.ok(result);
}
@GetMapping("/{id}")
public ResponseEntity<BankDTO> findById(@PathVariable(name = "id") UUID id) {
try {
BankDTO dto = bankService.findById(id);
return ResponseEntity.ok(dto);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public ResponseEntity<BankDTO> create(@Valid @RequestBody BankDTO dto) {
try {
BankDTO created = bankService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<BankDTO> update(
@PathVariable(name = "id") UUID id,
@Valid @RequestBody BankDTO dto) {
try {
BankDTO updated = bankService.update(id, dto);
return ResponseEntity.ok(updated);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
}
@@ -0,0 +1,34 @@
package br.gov.sigefp.common.api.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* DTO para transferência de dados de banco.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BankDTO {
private UUID id;
@NotBlank(message = "Código é obrigatório")
@Size(min = 1, max = 20, message = "Código deve ter entre 1 e 20 caracteres")
private String code;
@NotBlank(message = "Nome é obrigatório")
@Size(max = 200, message = "Nome deve ter no máximo 200 caracteres")
private String name;
@Size(max = 20, message = "Código SWIFT deve ter no máximo 20 caracteres")
private String swiftCode;
}
@@ -0,0 +1,29 @@
package br.gov.sigefp.common.api.dto;
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDateTime;
import java.util.List;
/**
* Standard DTO for API error responses.
*/
@Getter
@Builder
public class ErrorResponse {
private String timestamp;
private int status;
private String error;
private String code;
private String message;
private String path;
private List<ValidationError> errors;
@Getter
@Builder
public static class ValidationError {
private String field;
private String message;
}
}
@@ -0,0 +1,97 @@
package br.gov.sigefp.common.api.exception;
import br.gov.sigefp.common.api.dto.ErrorResponse;
import br.gov.sigefp.common.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* Global exception handler for all modules.
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex, HttpServletRequest request) {
log.warn("Business rule violation: {} - Code: {}", ex.getMessage(), ex.getCode());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now().format(formatter))
.status(ex.getStatus().value())
.error(ex.getStatus().getReasonPhrase())
.code(ex.getCode())
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return new ResponseEntity<>(error, ex.getStatus());
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex, HttpServletRequest request) {
log.warn("Illegal argument: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now().format(formatter))
.status(400)
.error("Bad Request")
.code("ILLEGAL_ARGUMENT")
.message(ex.getMessage())
.path(request.getRequestURI())
.build();
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<ErrorResponse.ValidationError> validationErrors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> ErrorResponse.ValidationError.builder()
.field(error.getField())
.message(error.getDefaultMessage())
.build())
.collect(Collectors.toList());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now().format(formatter))
.status(400)
.error("Bad Request")
.code("VALIDATION_ERROR")
.message("Validation failed")
.path(request.getRequestURI())
.errors(validationErrors)
.build();
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex, HttpServletRequest request) {
log.error("Internal Server Error: ", ex);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now().format(formatter))
.status(500)
.error("Internal Server Error")
.code("INTERNAL_ERROR")
.message("An unexpected error occurred")
.path(request.getRequestURI())
.build();
return ResponseEntity.internalServerError().body(error);
}
}
@@ -0,0 +1,27 @@
package br.gov.sigefp.common.application;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO para requisições de paginação.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageRequest {
@Builder.Default
private int page = 0;
@Builder.Default
private int size = 20;
private String sortBy;
private String sortDirection; // ASC, DESC
}
@@ -0,0 +1,59 @@
package br.gov.sigefp.common.domain;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.Instant;
import java.util.UUID;
/**
* Entidade global para auditoria de ações no sistema.
* Desacoplada de UserAccount para permitir uso em todos os módulos.
*/
@Entity
@Table(name = "audit_log", indexes = {
@Index(name = "idx_audit_user_id", columnList = "user_id"),
@Index(name = "idx_audit_module", columnList = "module"),
@Index(name = "idx_audit_entity", columnList = "entity,entity_id"),
@Index(name = "idx_audit_created", columnList = "created_at")
})
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(updatable = false, nullable = false)
private UUID id;
@Column(name = "user_id")
private UUID userId;
@Column(name = "username", length = 100)
private String username;
@Column(nullable = false, length = 50)
private String module;
@Column(nullable = false, length = 50)
private String action;
@Column(nullable = false, length = 100)
private String entity;
@Column(name = "entity_id")
private UUID entityId;
@Column(length = 2000)
private String description;
@CreatedDate
@Column(nullable = false, updatable = false, name = "created_at")
private Instant createdAt;
}
@@ -0,0 +1,29 @@
package br.gov.sigefp.common.domain;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.UUID;
/**
* Entidade base com campos de auditoria (quem criou/modificou).
*/
@MappedSuperclass
@Getter
@Setter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class AuditableEntity extends BaseEntity {
@Column(name = "created_by")
private UUID createdBy;
@Column(name = "updated_by")
private UUID updatedBy;
}
@@ -0,0 +1,33 @@
package br.gov.sigefp.common.domain;
import jakarta.persistence.*;
import lombok.*;
/**
* Entidade que representa um banco.
* Será usada por:
* - rh.agent_bank_account (ligação de agentes a contas bancárias)
* - treasury/payment (para identificar bancos em ordens de pagamento)
*/
@Entity
@Table(name = "bank", indexes = {
@Index(name = "idx_bank_code", columnList = "code"),
@Index(name = "idx_bank_swift", columnList = "swift_code")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Bank extends BaseEntity {
@Column(nullable = false, unique = true, length = 20)
private String code; // Código do banco (ex: 001, 237, etc.)
@Column(nullable = false, length = 200)
private String name; // Nome do banco
@Column(length = 20, name = "swift_code")
private String swiftCode; // Código SWIFT/BIC (opcional)
}
@@ -0,0 +1,45 @@
package br.gov.sigefp.common.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Entidade base com campos comuns a todas as entidades do sistema.
* Usa UUID como identificador para evitar problemas de sincronização em
* ambientes distribuídos.
*/
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(updatable = false, nullable = false)
private UUID id;
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
@Version
private Long version;
}
@@ -0,0 +1,57 @@
package br.gov.sigefp.common.domain;
import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
/**
* Value Object para representar um período (ano-mês).
* Evita concatenar strings para datas e garante consistência.
*/
@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class PeriodId {
private Integer year;
private Integer month;
public static PeriodId of(int year, int month) {
if (month < 1 || month > 12) {
throw new IllegalArgumentException("Mês deve estar entre 1 e 12");
}
if (year < 2000 || year > 2100) {
throw new IllegalArgumentException("Ano deve estar entre 2000 e 2100");
}
return new PeriodId(year, month);
}
public static PeriodId of(YearMonth yearMonth) {
return new PeriodId(yearMonth.getYear(), yearMonth.getMonthValue());
}
public static PeriodId current() {
return of(YearMonth.now());
}
public YearMonth toYearMonth() {
return YearMonth.of(year, month);
}
public String toString() {
return String.format("%04d-%02d", year, month);
}
public static PeriodId fromString(String period) {
YearMonth ym = YearMonth.parse(period, DateTimeFormatter.ofPattern("yyyy-MM"));
return of(ym);
}
}
@@ -0,0 +1,24 @@
package br.gov.sigefp.common.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
/**
* Base exception for business rule violations in the SIGEFP system.
*/
@Getter
public class BusinessException extends RuntimeException {
private final HttpStatus status;
private final String code;
public BusinessException(String message) {
this(message, "BUSINESS_ERROR", HttpStatus.BAD_REQUEST);
}
public BusinessException(String message, String code, HttpStatus status) {
super(message);
this.code = code;
this.status = status;
}
}
@@ -0,0 +1,13 @@
package br.gov.sigefp.common.exception;
import org.springframework.http.HttpStatus;
/**
* Exception thrown when there is no sufficient budget to perform an operation.
*/
public class InsufficientBudgetException extends BusinessException {
public InsufficientBudgetException(String message) {
super(message, "INSUFFICIENT_BUDGET", HttpStatus.UNPROCESSABLE_ENTITY);
}
}
@@ -0,0 +1,13 @@
package br.gov.sigefp.common.exception;
import org.springframework.http.HttpStatus;
/**
* Exception thrown when a requested resource is not found.
*/
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String message) {
super(message, "RESOURCE_NOT_FOUND", HttpStatus.NOT_FOUND);
}
}
@@ -0,0 +1,10 @@
package br.gov.sigefp.common.infrastructure.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
@@ -0,0 +1,25 @@
package br.gov.sigefp.common.repository;
import br.gov.sigefp.common.domain.AuditLog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.UUID;
@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, UUID> {
Page<AuditLog> findByUserId(UUID userId, Pageable pageable);
Page<AuditLog> findByModule(String module, Pageable pageable);
Page<AuditLog> findByCreatedAtBetween(Instant start, Instant end, Pageable pageable);
Page<AuditLog> findByUserIdAndModule(UUID userId, String module, Pageable pageable);
Page<AuditLog> findByUserIdAndModuleAndCreatedAtBetween(UUID userId, String module, Instant start, Instant end,
Pageable pageable);
}
@@ -0,0 +1,17 @@
package br.gov.sigefp.common.repository;
import br.gov.sigefp.common.domain.Bank;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface BankRepository extends JpaRepository<Bank, UUID> {
Optional<Bank> findByCode(String code);
boolean existsByCode(String code);
}
@@ -0,0 +1,91 @@
package br.gov.sigefp.common.service;
import br.gov.sigefp.common.api.dto.BankDTO;
import br.gov.sigefp.common.domain.Bank;
import br.gov.sigefp.common.repository.BankRepository;
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.UUID;
/**
* Serviço de aplicação para gestão de bancos.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class BankService {
private final BankRepository bankRepository;
public BankDTO create(BankDTO dto) {
// Validação: garantir unicidade de code
if (bankRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de banco já existe: " + dto.getCode());
}
Bank bank = Bank.builder()
.code(dto.getCode())
.name(dto.getName())
.swiftCode(dto.getSwiftCode())
.build();
Bank saved = bankRepository.save(bank);
return toDTO(saved);
}
public BankDTO update(UUID id, BankDTO dto) {
Bank bank = bankRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Banco não encontrado: " + id));
if (dto.getCode() != null && !dto.getCode().equals(bank.getCode())) {
// Validação: garantir unicidade de code
if (bankRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de banco já existe: " + dto.getCode());
}
bank.setCode(dto.getCode());
}
if (dto.getName() != null) {
bank.setName(dto.getName());
}
if (dto.getSwiftCode() != null) {
bank.setSwiftCode(dto.getSwiftCode());
}
Bank saved = bankRepository.save(bank);
return toDTO(saved);
}
@Transactional(readOnly = true)
public BankDTO findById(UUID id) {
Bank bank = bankRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Banco não encontrado: " + id));
return toDTO(bank);
}
@Transactional(readOnly = true)
public BankDTO findByCode(String code) {
Bank bank = bankRepository.findByCode(code)
.orElseThrow(() -> new IllegalArgumentException("Banco não encontrado com código: " + code));
return toDTO(bank);
}
@Transactional(readOnly = true)
public Page<BankDTO> findAll(Pageable pageable) {
return bankRepository.findAll(pageable).map(this::toDTO);
}
private BankDTO toDTO(Bank bank) {
return BankDTO.builder()
.id(bank.getId())
.code(bank.getCode())
.name(bank.getName())
.swiftCode(bank.getSwiftCode())
.build();
}
}
@@ -0,0 +1,47 @@
package br.gov.sigefp.common.service;
import br.gov.sigefp.common.domain.AuditLog;
import br.gov.sigefp.common.repository.AuditLogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/**
* Serviço global para registro de logs de auditoria.
* Disponível para todos os módulos do sistema.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class GlobalAuditLogService {
private final AuditLogRepository auditLogRepository;
/**
* Registra uma ação no log de auditoria.
*
* @param userId ID do utilizador (opcional)
* @param username Nome do utilizador (opcional)
* @param module Módulo (ADMIN, ORG, RH, etc)
* @param action Ação (CREATE, UPDATE, DELETE, etc)
* @param entity Nome da entidade objeto
* @param entityId ID da entidade objeto
* @param description Descrição detalhada (Ex: Campo: [Antigo] -> [Novo])
*/
public void logAction(UUID userId, String username, String module, String action, String entity, UUID entityId,
String description) {
AuditLog log = AuditLog.builder()
.userId(userId)
.username(username)
.module(module)
.action(action)
.entity(entity)
.entityId(entityId)
.description(description)
.build();
auditLogRepository.save(log);
}
}
@@ -0,0 +1,18 @@
package br.gov.sigefp.common.service;
import java.util.UUID;
/**
* Interface para geração de pagamentos, permitindo inversão de dependência
* entre módulos RH e Tesouraria.
*/
public interface PaymentGenerator {
/**
* Gera ordens de pagamento a partir de uma execução de folha.
*
* @param payrollRunId ID da execução de folha
* @param paymentBatchId ID do lote de pagamento (opcional, pode ser null para
* criar novo)
*/
void generateOrdersFromPayrollRun(UUID payrollRunId, UUID paymentBatchId);
}
@@ -0,0 +1,30 @@
package br.gov.sigefp.common.service;
import java.util.List;
import java.util.Map;
/**
* Serviço genérico para geração de relatórios.
*/
public interface ReportService {
/**
* Gera um relatório em formato Excel (XLSX).
*
* @param data Lista de dados a serem exportados.
* @param headers Mapa de chaves (propriedades) e títulos das colunas.
* @param sheetName Nome da aba na planilha.
* @return Byte array do arquivo gerado.
*/
byte[] generateExcel(List<?> data, Map<String, String> headers, String sheetName);
/**
* Gera um relatório em formato PDF.
*
* @param data Lista de dados a serem exportados.
* @param headers Mapa de chaves (propriedades) e títulos das colunas.
* @param title Título do relatório.
* @return Byte array do arquivo gerado.
*/
byte[] generatePdf(List<?> data, Map<String, String> headers, String title);
}
@@ -0,0 +1,240 @@
package br.gov.sigefp.common.service;
import com.lowagie.text.Document;
import com.lowagie.text.Element;
import com.lowagie.text.FontFactory;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Override
public byte[] generateExcel(List<?> data, Map<String, String> headers, String sheetName) {
try (Workbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet(sheetName);
// Create Header Row
Row headerRow = sheet.createRow(0);
CellStyle headerStyle = workbook.createCellStyle();
org.apache.poi.ss.usermodel.Font font = workbook.createFont();
font.setBold(true);
headerStyle.setFont(font);
int col = 0;
for (String title : headers.values()) {
Cell cell = headerRow.createCell(col++);
cell.setCellValue(title);
cell.setCellStyle(headerStyle);
}
// Populate Data
int rowIdx = 1;
for (Object item : data) {
Row row = sheet.createRow(rowIdx++);
col = 0;
for (String fieldName : headers.keySet()) {
Cell cell = row.createCell(col++);
Object value = getFieldValue(item, fieldName);
if (value != null) {
cell.setCellValue(formatValue(value));
}
}
}
workbook.write(out);
return out.toByteArray();
} catch (Exception e) {
log.error("Erro ao gerar relatório Excel", e);
throw new RuntimeException("Falha na geração do Excel", e);
}
}
@Override
public byte[] generatePdf(List<?> data, Map<String, String> headers, String title) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Document document = new Document(PageSize.A4.rotate());
PdfWriter.getInstance(document, out);
document.open();
// --- Cabelho Institucional ---
// Fonte para o cabeçalho
com.lowagie.text.Font headerFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 12);
com.lowagie.text.Font subHeaderFont = FontFactory.getFont(FontFactory.HELVETICA, 10);
Paragraph textHeader = new Paragraph();
textHeader.setAlignment(Element.ALIGN_CENTER);
textHeader.add(new Phrase("REPÚBLICA DA GUINÉ-BISSAU\n", headerFont));
textHeader.add(new Phrase("MINISTÉRIO DA ECONOMIA E FINANÇAS\n", headerFont));
textHeader.add(new Phrase("DIREÇÃO GERAL DO ORÇAMENTO\n", subHeaderFont));
textHeader.add(new Phrase("SIGEFP - SISTEMA INTEGRADO DE GESTÃO FINANCEIRA\n", subHeaderFont));
textHeader.setSpacingAfter(20);
document.add(textHeader);
// Linha separadora
com.lowagie.text.pdf.draw.LineSeparator ls = new com.lowagie.text.pdf.draw.LineSeparator();
document.add(new Paragraph(new com.lowagie.text.Chunk(ls)));
document.add(new Paragraph(" ")); // Espaço
// --- Título do Relatório ---
com.lowagie.text.Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14);
Paragraph titlePara = new Paragraph(title.toUpperCase(), titleFont);
titlePara.setAlignment(Element.ALIGN_CENTER);
titlePara.setSpacingAfter(15);
document.add(titlePara);
// Data de Emissão
Paragraph datePara = new Paragraph(
"Emitido em: "
+ java.time.LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")),
FontFactory.getFont(FontFactory.HELVETICA, 8));
datePara.setAlignment(Element.ALIGN_RIGHT);
datePara.setSpacingAfter(10);
document.add(datePara);
// --- Tabela de Dados ---
PdfPTable table = new PdfPTable(headers.size());
table.setWidthPercentage(100);
table.setSpacingBefore(10f);
table.setSpacingAfter(10f);
// Estilo do Cabeçalho da Tabela
com.lowagie.text.Font tableHeaderFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 10,
java.awt.Color.WHITE);
for (String headerTitle : headers.values()) {
PdfPCell cell = new PdfPCell(new Phrase(headerTitle, tableHeaderFont));
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
cell.setBackgroundColor(java.awt.Color.DARK_GRAY);
cell.setPadding(5);
table.addCell(cell);
}
// Estilo dos Dados
com.lowagie.text.Font dataFont = FontFactory.getFont(FontFactory.HELVETICA, 9);
boolean alternate = false;
java.awt.Color lightGray = new java.awt.Color(240, 240, 240);
for (Object item : data) {
for (String fieldName : headers.keySet()) {
Object value = getFieldValue(item, fieldName);
PdfPCell cell = new PdfPCell(new Phrase(value != null ? formatValue(value) : "", dataFont));
cell.setPadding(4);
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
if (alternate) {
cell.setBackgroundColor(lightGray);
}
table.addCell(cell);
}
alternate = !alternate;
}
document.add(table);
// --- Rodapé Simples ---
Paragraph footer = new Paragraph(
"Documento processado eletronicamente pelo SIGEFP. A autenticidade pode ser verificada no sistema.",
FontFactory.getFont(FontFactory.HELVETICA_OBLIQUE, 8));
footer.setAlignment(Element.ALIGN_CENTER);
footer.setSpacingBefore(20);
document.add(footer);
document.close();
return out.toByteArray();
} catch (Exception e) {
log.error("Erro ao gerar relatório PDF", e);
throw new RuntimeException("Falha na geração do PDF", e);
}
}
// Helper to get field value via reflection
private Object getFieldValue(Object object, String fieldName) {
if (object == null || fieldName == null) {
return null;
}
// Suporte para Map (colunas dinâmicas)
if (object instanceof Map) {
return ((Map<?, ?>) object).get(fieldName);
}
// Primeiro, tentar via getter (mais seguro com Lombok @Data)
try {
String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
java.lang.reflect.Method getter = object.getClass().getMethod(getterName);
return getter.invoke(object);
} catch (NoSuchMethodException e) {
// Tentar campo direto
} catch (Exception e) {
log.debug("Erro ao invocar getter para {}: {}", fieldName, e.getMessage());
}
// Fallback: tentar campo direto
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
} catch (NoSuchFieldException e) {
log.debug("Campo não encontrado: {}", fieldName);
return null;
} catch (IllegalAccessException e) {
log.debug("Erro ao acessar campo {}: {}", fieldName, e.getMessage());
return null;
}
}
private String formatValue(Object value) {
if (value == null)
return "";
if (value instanceof java.time.LocalDate) {
return ((java.time.LocalDate) value).format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
} else if (value instanceof java.time.LocalDateTime) {
return ((java.time.LocalDateTime) value).format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"));
} else if (value instanceof java.math.BigDecimal) {
// Formatar moeda (XOF/BCEAO geralmente usa espaço como separador de milhar e
// vírgula ou ponto, vamos usar padrão PT-BR para consistência com o frontend)
java.text.NumberFormat nf = java.text.NumberFormat.getInstance(new java.util.Locale("pt", "BR"));
nf.setMinimumFractionDigits(2);
return nf.format(value);
} else if (value instanceof String) {
// Traduções rápidas para valores conhecidos
String s = (String) value;
return switch (s) {
case "COMMITMENT" -> "Empenho";
case "LIQUIDATION" -> "Liquidação";
case "PAYMENT" -> "Pagamento";
case "INITIAL_ALLOCATION" -> "Dotação Inicial";
case "SUPPLEMENTARY_CREDIT" -> "Crédito Suplementar";
default -> s;
};
}
return value.toString();
}
}
@@ -0,0 +1,157 @@
package br.gov.sigefp.common.util;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
/**
* Configurações e utilitários específicos para Guiné-Bissau.
*/
public class GuineaBissauConfig {
/**
* Código do país: GW (ISO 3166-1 alpha-2)
*/
public static final String COUNTRY_CODE = "GW";
/**
* Nome do país
*/
public static final String COUNTRY_NAME = "Guiné-Bissau";
/**
* Código da moeda: XOF (Franco CFA da África Ocidental)
*/
public static final String CURRENCY_CODE = "XOF";
/**
* Símbolo da moeda
*/
public static final String CURRENCY_SYMBOL = "FCFA";
/**
* Timezone: Africa/Bissau (UTC+0)
*/
public static final ZoneId TIMEZONE = ZoneId.of("Africa/Bissau");
/**
* Locale: Português da Guiné-Bissau
*/
public static final Locale LOCALE = new Locale("pt", "GW");
/**
* Formato de data: DD/MM/YYYY
*/
public static final String DATE_FORMAT = "dd/MM/yyyy";
/**
* Formato de data e hora: DD/MM/YYYY HH:mm
*/
public static final String DATETIME_FORMAT = "dd/MM/yyyy HH:mm";
/**
* Formato de data e hora completo: DD/MM/YYYY HH:mm:ss
*/
public static final String DATETIME_FULL_FORMAT = "dd/MM/yyyy HH:mm:ss";
/**
* Código telefônico internacional: +245
*/
public static final String PHONE_CODE = "+245";
/**
* Formata um valor monetário no formato da Guiné-Bissau.
*
* @param value Valor a formatar
* @return String formatada (ex: "1.234,56 FCFA")
*/
public static String formatCurrency(BigDecimal value) {
if (value == null) {
return "0,00 " + CURRENCY_SYMBOL;
}
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(LOCALE);
// Como XOF não tem suporte nativo em todos os Locales, formatamos manualmente
NumberFormat numberFormat = NumberFormat.getNumberInstance(LOCALE);
numberFormat.setMinimumFractionDigits(2);
numberFormat.setMaximumFractionDigits(2);
return numberFormat.format(value) + " " + CURRENCY_SYMBOL;
}
/**
* Formata um número no formato da Guiné-Bissau.
*
* @param value Valor a formatar
* @return String formatada (ex: "1.234,56")
*/
public static String formatNumber(BigDecimal value) {
if (value == null) {
return "0,00";
}
NumberFormat numberFormat = NumberFormat.getNumberInstance(LOCALE);
numberFormat.setMinimumFractionDigits(2);
numberFormat.setMaximumFractionDigits(2);
return numberFormat.format(value);
}
/**
* Formata um número inteiro no formato da Guiné-Bissau.
*
* @param value Valor a formatar
* @return String formatada (ex: "1.234")
*/
public static String formatInteger(Long value) {
if (value == null) {
return "0";
}
NumberFormat numberFormat = NumberFormat.getNumberInstance(LOCALE);
numberFormat.setMinimumFractionDigits(0);
numberFormat.setMaximumFractionDigits(0);
return numberFormat.format(value);
}
/**
* Retorna um DateTimeFormatter para datas no formato DD/MM/YYYY.
*/
public static DateTimeFormatter getDateFormatter() {
return DateTimeFormatter.ofPattern(DATE_FORMAT, LOCALE);
}
/**
* Retorna um DateTimeFormatter para data e hora no formato DD/MM/YYYY HH:mm.
*/
public static DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(DATETIME_FORMAT, LOCALE);
}
/**
* Retorna um DateTimeFormatter para data e hora completo no formato DD/MM/YYYY HH:mm:ss.
*/
public static DateTimeFormatter getDateTimeFullFormatter() {
return DateTimeFormatter.ofPattern(DATETIME_FULL_FORMAT, LOCALE);
}
/**
* Retorna um SimpleDateFormat para datas no formato DD/MM/YYYY.
*/
public static SimpleDateFormat getSimpleDateFormat() {
return new SimpleDateFormat(DATE_FORMAT, LOCALE);
}
/**
* Retorna um SimpleDateFormat para data e hora no formato DD/MM/YYYY HH:mm.
*/
public static SimpleDateFormat getSimpleDateTimeFormat() {
return new SimpleDateFormat(DATETIME_FORMAT, LOCALE);
}
}