# 🔧 Correções Críticas Necessárias - Módulo de Orçamento **Data:** 2025-01-XX **Prioridade:** 🔴 CRÍTICO **Baseado em:** `ANALISE_TECNICA_PROFUNDA_ORCAMENTO.md` --- ## 🚨 Problema Crítico #1: Cálculo Incorreto de totalCommitted ### Problema Identificado **Arquivo:** `sigefp-budget/src/main/java/br/gov/sigefp/budget/domain/BudgetLine.java` (Linha 49) ```java // ❌ ERRADO - Soma TODOS os tipos de movimento @org.hibernate.annotations.Formula("(SELECT COALESCE(SUM(bex.amount), 0) FROM budget_execution bex WHERE bex.budget_line_id = id)") private BigDecimal totalCommitted; ``` **Impacto:** - Soma COMMITMENT + LIQUIDATION + PAYMENT - `availableBalance = totalAllocated - totalCommitted` fica **INCORRETO** - Permite criar COMMITMENTs além do disponível - **Quebra integridade orçamentária** ### Correção ```java // ✅ CORRETO - Soma apenas COMMITMENT @org.hibernate.annotations.Formula("(SELECT COALESCE(SUM(bex.amount), 0) FROM budget_execution bex WHERE bex.budget_line_id = id AND bex.movement_type = 'COMMITMENT')") private BigDecimal totalCommitted; ``` **Nota:** O `BudgetExecutionRepository.calculateTotalCommittedByBudgetLineId()` já está correto (filtra por COMMITMENT), mas o @Formula não. --- ## 🚨 Problema Crítico #2: Falta Validação de Sequência ### Problema Identificado **Arquivo:** `sigefp-budget/src/main/java/br/gov/sigefp/budget/service/BudgetExecutionService.java` **Cenário Atual:** - ✅ Valida COMMITMENT (saldo disponível) - ❌ **NÃO valida** se existe COMMITMENT antes de criar LIQUIDATION - ❌ **NÃO valida** se existe LIQUIDATION antes de criar PAYMENT - ❌ Permite criar PAYMENT sem COMMITMENT/LIQUIDATION **Impacto:** - Quebra ciclo de execução orçamentária (COMMITMENT → LIQUIDATION → PAYMENT) - Permite "pular" etapas - **Não conforma com normas GFP** ### Correção Necessária Adicionar validações no método `registerExecution()`: ```java // Após validação de COMMITMENT (linha 59) // Validação 3: Sequência para LIQUIDATION if ("LIQUIDATION".equals(dto.getMovementType())) { if (dto.getReferenceId() == null) { throw new BusinessException("ReferenceId é obrigatório para LIQUIDATION", "MISSING_REFERENCE_ID", HttpStatus.BAD_REQUEST); } // Buscar COMMITMENT correspondente List commitments = budgetExecutionRepository .findByBudgetLineIdAndMovementType(budgetLine.getId(), "COMMITMENT"); // Filtrar por referenceId se fornecido BigDecimal totalCommitted = commitments.stream() .filter(c -> dto.getReferenceId().equals(c.getReferenceId())) .map(BudgetExecution::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); if (totalCommitted.compareTo(BigDecimal.ZERO) == 0) { throw new BusinessException("Não existe COMMITMENT correspondente para liquidar", "NO_COMMITMENT", HttpStatus.CONFLICT); } // Calcular total já liquidado List liquidations = budgetExecutionRepository .findByBudgetLineIdAndMovementType(budgetLine.getId(), "LIQUIDATION"); BigDecimal totalLiquidated = liquidations.stream() .filter(l -> dto.getReferenceId().equals(l.getReferenceId())) .map(BudgetExecution::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal availableToLiquidate = totalCommitted.subtract(totalLiquidated); if (dto.getAmount().compareTo(availableToLiquidate) > 0) { throw new BusinessException( String.format("Liquidação não pode exceder empenho. Disponível: %s, Solicitado: %s", availableToLiquidate, dto.getAmount()), "INSUFFICIENT_COMMITMENT", HttpStatus.CONFLICT); } } // Validação 4: Sequência para PAYMENT if ("PAYMENT".equals(dto.getMovementType())) { if (dto.getReferenceId() == null) { throw new BusinessException("ReferenceId é obrigatório para PAYMENT", "MISSING_REFERENCE_ID", HttpStatus.BAD_REQUEST); } // Buscar LIQUIDATION correspondente List liquidations = budgetExecutionRepository .findByBudgetLineIdAndMovementType(budgetLine.getId(), "LIQUIDATION"); BigDecimal totalLiquidated = liquidations.stream() .filter(l -> dto.getReferenceId().equals(l.getReferenceId())) .map(BudgetExecution::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); if (totalLiquidated.compareTo(BigDecimal.ZERO) == 0) { throw new BusinessException("Não existe LIQUIDATION correspondente para pagar", "NO_LIQUIDATION", HttpStatus.CONFLICT); } // Calcular total já pago List payments = budgetExecutionRepository .findByBudgetLineIdAndMovementType(budgetLine.getId(), "PAYMENT"); BigDecimal totalPaid = payments.stream() .filter(p -> dto.getReferenceId().equals(p.getReferenceId())) .map(BudgetExecution::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal availableToPay = totalLiquidated.subtract(totalPaid); if (dto.getAmount().compareTo(availableToPay) > 0) { throw new BusinessException( String.format("Pagamento não pode exceder liquidação. Disponível: %s, Solicitado: %s", availableToPay, dto.getAmount()), "INSUFFICIENT_LIQUIDATION", HttpStatus.CONFLICT); } } ``` **Nota:** Para melhor performance, seria ideal criar métodos no repository: - `calculateTotalCommittedByReferenceId(UUID referenceId)` - `calculateTotalLiquidatedByReferenceId(UUID referenceId)` - `calculateTotalPaidByReferenceId(UUID referenceId)` --- ## 🟡 Problema Médio #3: Tratamento de Exceções ### Correção BudgetEntryService **Arquivo:** `sigefp-budget/src/main/java/br/gov/sigefp/budget/service/BudgetEntryService.java` ```java // ❌ ANTES .orElseThrow(() -> new IllegalArgumentException("Linha orçamentária não encontrada: " + dto.getBudgetLineId())); // ✅ DEPOIS import br.gov.sigefp.common.exception.ResourceNotFoundException; .orElseThrow(() -> new ResourceNotFoundException("Linha orçamentária não encontrada: " + dto.getBudgetLineId())); ``` ### Correção BudgetEntryController **Arquivo:** `sigefp-budget/src/main/java/br/gov/sigefp/budget/api/BudgetEntryController.java` ```java // ❌ ANTES catch (IllegalArgumentException e) { return ResponseEntity.badRequest().build(); } // ✅ DEPOIS import br.gov.sigefp.common.exception.ResourceNotFoundException; import br.gov.sigefp.common.exception.BusinessException; import lombok.extern.slf4j.Slf4j; @Slf4j public class BudgetEntryController { // ... @PostMapping public ResponseEntity create(@Valid @RequestBody CreateBudgetEntryDTO dto) { try { BudgetEntryDTO created = budgetEntryService.create(dto); return ResponseEntity.status(HttpStatus.CREATED).body(created); } catch (ResourceNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } catch (BusinessException e) { return ResponseEntity.status(e.getHttpStatus()).build(); } catch (Exception e) { log.error("Erro inesperado ao criar entrada orçamentária", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } } ``` --- ## 🟡 Problema Médio #4: BudgetIntegrationService **Arquivo:** `sigefp-budget/src/main/java/br/gov/sigefp/budget/integration/BudgetIntegrationService.java` ```java // ❌ ANTES catch (Exception e) { log.error("Erro ao criar execução orçamentária (COMMITMENT): {}", e.getMessage()); throw new RuntimeException("Erro ao criar execução orçamentária: " + e.getMessage(), e); } // ✅ DEPOIS catch (InsufficientBudgetException e) { log.error("Saldo insuficiente ao criar execução orçamentária (COMMITMENT)", e); throw e; // Re-throw exceção específica } catch (BusinessException e) { log.error("Erro de negócio ao criar execução orçamentária (COMMITMENT)", e); throw e; } catch (Exception e) { log.error("Erro inesperado ao criar execução orçamentária (COMMITMENT)", e); throw new BusinessException("Erro ao criar execução orçamentária", "EXECUTION_ERROR", HttpStatus.INTERNAL_SERVER_ERROR); } ``` --- ## 🟡 Problema Médio #5: Validação de TRANSFER_OUT **Arquivo:** `sigefp-budget/src/main/java/br/gov/sigefp/budget/service/BudgetEntryService.java` **Problema:** Não valida se TRANSFER_OUT não excede saldo disponível. **Correção:** ```java public BudgetEntryDTO create(CreateBudgetEntryDTO dto) { BudgetLine budgetLine = budgetLineRepository.findById(dto.getBudgetLineId()) .orElseThrow(() -> new ResourceNotFoundException( "Linha orçamentária não encontrada: " + dto.getBudgetLineId())); // Validação: TRANSFER_OUT não pode exceder saldo disponível if (dto.getType() == BudgetEntryType.TRANSFER_OUT || dto.getType() == BudgetEntryType.CANCELLATION) { BigDecimal totalAllocated = budgetAllocationRepository .calculateTotalAllocatedByBudgetLineId(budgetLine.getId()); BigDecimal totalCommitted = budgetExecutionRepository .calculateTotalCommittedByBudgetLineId(budgetLine.getId()); BigDecimal available = totalAllocated.subtract(totalCommitted); if (dto.getAmount().compareTo(available) > 0) { throw new BusinessException( String.format("Transferência/Cancelamento não pode exceder saldo disponível. Disponível: %s, Solicitado: %s", available, dto.getAmount()), "INSUFFICIENT_BALANCE", HttpStatus.CONFLICT); } } // ... resto do código } ``` --- ## 📋 Resumo das Correções ### 🔴 Críticas (Urgente) 1. ✅ Corrigir `@Formula` de `totalCommitted` para filtrar apenas COMMITMENT 2. ✅ Adicionar validação de sequência LIQUIDATION (exige COMMITMENT) 3. ✅ Adicionar validação de sequência PAYMENT (exige LIQUIDATION) ### 🟡 Médias (Importante) 4. ✅ Substituir `IllegalArgumentException` por exceções customizadas 5. ✅ Melhorar tratamento de erros nos controllers 6. ✅ Melhorar `BudgetIntegrationService` (re-throw exceções específicas) 7. ✅ Adicionar validação de TRANSFER_OUT/CANCELLATION --- **Documento gerado em:** 2025-01-XX **Versão:** 1.0