From 25b440b842c52bb0d9e01e471eaf764ee5ece6ff Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Mon, 28 Jul 2025 13:45:43 +0300 Subject: [PATCH] FINERACT-2317: Include capitalized income and buy down fee in available disbursement calculation --- .../DelinquencyReadPlatformServiceImpl.java | 22 +++++++++++++++---- ...talizedIncomeWritePlatformServiceImpl.java | 2 +- .../LoanDisbursementValidator.java | 21 +++++++----------- ...ntDelinquencyRangeEventSerializerTest.java | 6 +++++ 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 62bbbe2fa..7c2fd3fcc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -58,6 +58,7 @@ import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquenc import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.springframework.lang.NonNull; @@ -149,10 +150,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo // Overpaid // loans collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList); - // Ensure availableDisbursementAmount is never negative - final BigDecimal availableAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()); - collectionData - .setAvailableDisbursementAmount(availableAmount.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : availableAmount); + collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan)); collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan)); final LoanTransaction lastPayment = loan.getLastPaymentTransaction(); @@ -177,6 +175,22 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo return collectionData; } + private BigDecimal calculateAvailableDisbursementAmount(@NonNull final Loan loan) { + BigDecimal availableDisbursementAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()); + if (loan.getLoanRepaymentScheduleDetail().isEnableIncomeCapitalization()) { + final LoanSummary loanSummary = loan.getSummary(); + if (loanSummary != null) { + final BigDecimal totalCapitalizedIncome = MathUtil.nullToZero(loanSummary.getTotalCapitalizedIncome()); + final BigDecimal totalCapitalizedIncomeAdjustment = MathUtil.nullToZero(loanSummary.getTotalCapitalizedIncomeAdjustment()); + final BigDecimal netCapitalizedIncome = totalCapitalizedIncome.subtract(totalCapitalizedIncomeAdjustment); + availableDisbursementAmount = availableDisbursementAmount.subtract(netCapitalizedIncome); + } + } + + // Ensure availableDisbursementAmount is never negative + return availableDisbursementAmount.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : availableDisbursementAmount; + } + private void addInstallmentLevelDelinquencyData(CollectionData collectionData, Long loanId) { Collection loanInstallmentDelinquencyTagData = retrieveLoanInstallmentsCurrentDelinquencyTag( loanId); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java index 6a7e6d484..9a34fe4b9 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java @@ -164,7 +164,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc .setAmountAdjustment(MathUtil.nullToZero(capitalizedIncomeBalance.getAmountAdjustment()).add(transactionAmount)); capitalizedIncomeBalance.setUnrecognizedAmount( MathUtil.negativeToZero(capitalizedIncomeBalance.getUnrecognizedAmount().subtract(transactionAmount))); - capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance); + capitalizedIncomeBalanceRepository.saveAndFlush(capitalizedIncomeBalance); loanLifecycleStateMachine.determineAndTransition(loan, transactionDate); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java index 69be1ab51..fe2fa363e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java @@ -22,6 +22,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException; @@ -35,15 +36,18 @@ public final class LoanDisbursementValidator { public void compareDisbursedToApprovedOrProposedPrincipal(final Loan loan, final BigDecimal disbursedAmount, final BigDecimal totalDisbursed) { - final BigDecimal capitalizedIncome = loan.getSummary().getTotalCapitalizedIncome(); + final BigDecimal totalCapitalizedIncome = loan.getSummary().getTotalCapitalizedIncome(); + final BigDecimal totalCapitalizedIncomeAdjustment = MathUtil.nullToZero(loan.getSummary().getTotalCapitalizedIncomeAdjustment()); + final BigDecimal netCapitalizedIncome = totalCapitalizedIncome.subtract(totalCapitalizedIncomeAdjustment); + if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - validateOverMaximumAmount(loan, totalDisbursed, capitalizedIncome); + validateOverMaximumAmount(loan, totalDisbursed, netCapitalizedIncome); } else { if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - validateOverMaximumAmount(loan, disbursedAmount, capitalizedIncome); + validateOverMaximumAmount(loan, disbursedAmount, netCapitalizedIncome); } else { if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0) - || (totalDisbursed.add(capitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) { + || (totalDisbursed.add(netCapitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) { final String errorMsg = "Loan can't be disbursed, disburse amount is exceeding approved principal."; throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed, loan.getApprovedPrincipal()); @@ -65,15 +69,6 @@ public final class LoanDisbursementValidator { } } - public void validateDisburseAmountNotExceedingApprovedAmount(final Loan loan, final BigDecimal diff, - final BigDecimal principalDisbursed) { - if (!loan.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) { - final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount "; - throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed, - loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount()); - } - } - public void validateDisburseDate(final Loan loan, final LocalDate disbursedOn, final LocalDate expectedDate) { if (expectedDate != null && (DateUtils.isAfter(disbursedOn, loan.fetchRepaymentScheduleInstallment(1).getDueDate()) diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index 9d41557b7..562ae63b9 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -100,6 +100,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -367,9 +368,14 @@ public class LoanAccountDelinquencyRangeEventSerializerTest { LoanProduct loanProduct = Mockito.mock(LoanProduct.class); when(loanProduct.isMultiDisburseLoan()).thenReturn(false); + + LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class); + when(loanProductRelatedDetail.isEnableIncomeCapitalization()).thenReturn(false); + Loan loan = Mockito.spy(Loan.class); ReflectionTestUtils.setField(loan, "loanProduct", loanProduct); ReflectionTestUtils.setField(loan, "loanStatus", LoanStatus.ACTIVE); + when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanProductRelatedDetail); LoanTransaction transaction1 = Mockito.mock(LoanTransaction.class); LoanTransaction transaction2 = Mockito.mock(LoanTransaction.class); CollectionData collectionData = Mockito.mock(CollectionData.class);