From bb2993265862636ff534004ce9125bd8a01db89f Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Wed, 11 Jun 2025 21:46:32 -0500 Subject: [PATCH] FINERACT-2308: Allow approval and disbursal above loan applied amount with non multibursal loan product --- .../loanproduct/domain/LoanProduct.java | 2 +- .../LoanApplicationValidator.java | 2 +- .../LoanDisbursementValidator.java | 38 ++++++++----- .../service/LoanDisbursementService.java | 5 +- ...ntAllocationLoanRepaymentScheduleTest.java | 55 +++++++++++++++++++ 5 files changed, 82 insertions(+), 20 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index f3dae22e9..b90217360 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -410,7 +410,7 @@ public class LoanProduct extends AbstractPersistableCustom { } if (this.allowApprovedDisbursedAmountsOverApplied) { - if (!this.disallowExpectedDisbursements) { + if (this.isMultiDisburseLoan() && !this.disallowExpectedDisbursements) { throw new LoanProductGeneralRuleException( "disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set", "Disallow Expected Disbursals Not Set - Allow Approved / Disbursed Amounts Over Applied Can't Be Set"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index e29934554..c6fc94d41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -2124,7 +2124,7 @@ public final class LoanApplicationValidator { } private void compareApprovedToProposedPrincipal(Loan loan, BigDecimal approvedLoanAmount) { - if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { BigDecimal maxApprovedLoanAmount = getOverAppliedMax(loan); if (approvedLoanAmount.compareTo(maxApprovedLoanAmount) > 0) { final String errorMessage = "Loan approved amount can't be greater than maximum applied loan amount calculation."; 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 2de131c56..6b5729230 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 @@ -37,26 +37,34 @@ public final class LoanDisbursementValidator { final BigDecimal totalDisbursed) { final BigDecimal capitalizedIncome = loan.getSummary().getTotalCapitalizedIncome(); if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - final BigDecimal maxDisbursedAmount = loanApplicationValidator.getOverAppliedMax(loan); - if (totalDisbursed.add(capitalizedIncome).compareTo(maxDisbursedAmount) > 0) { - final String errorMessage = String.format( - "Loan disbursal amount can't be greater than maximum applied loan amount calculation. " - + "Total disbursed amount: %s Maximum disbursal amount: %s", - totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString()); - throw new InvalidLoanStateTransitionException("disbursal", - "amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, disbursedAmount, - maxDisbursedAmount); - } + validateOverMaximumAmount(loan, totalDisbursed, capitalizedIncome); } else { - if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0) - || (totalDisbursed.add(capitalizedIncome).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()); + if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + validateOverMaximumAmount(loan, disbursedAmount, capitalizedIncome); + } else { + if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0) + || (totalDisbursed.add(capitalizedIncome).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()); + } } } } + public void validateOverMaximumAmount(final Loan loan, final BigDecimal totalDisbursed, final BigDecimal capitalizedIncome) { + final BigDecimal maxDisbursedAmount = loanApplicationValidator.getOverAppliedMax(loan); + if (totalDisbursed.add(capitalizedIncome).compareTo(maxDisbursedAmount) > 0) { + final String errorMessage = String.format( + "Loan disbursal amount can't be greater than maximum applied loan amount calculation. " + + "Total disbursed amount: %s Maximum disbursal amount: %s", + totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString()); + throw new InvalidLoanStateTransitionException("disbursal", + "amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, totalDisbursed, + maxDisbursedAmount); + } + } + public void validateDisburseAmountNotExceedingApprovedAmount(final Loan loan, final BigDecimal diff, final BigDecimal principalDisbursed) { if (!loan.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java index bb99b7087..8daee2dc6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java @@ -146,10 +146,10 @@ public class LoanDisbursementService { disbursementDetails.updatePrincipal(principalDisbursed); } } + BigDecimal totalAmount = BigDecimal.ZERO; if (loan.loanProduct().isMultiDisburseLoan()) { Collection loanDisburseDetails = loan.getDisbursementDetails(); BigDecimal setPrincipalAmount = BigDecimal.ZERO; - BigDecimal totalAmount = BigDecimal.ZERO; for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) { if (disbursementDetails.actualDisbursementDate() != null) { setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal()); @@ -157,12 +157,11 @@ public class LoanDisbursementService { totalAmount = totalAmount.add(disbursementDetails.principal()); } loan.getLoanRepaymentScheduleDetail().setPrincipal(setPrincipalAmount); - loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount); } else { loan.getLoanRepaymentScheduleDetail() .setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount()); } - loanDisbursementValidator.validateDisburseAmountNotExceedingApprovedAmount(loan, diff, principalDisbursed); + loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount); } return disburseAmount; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index d7334b44a..3f48344b8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -5946,6 +5946,61 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC155: Validate allowApprovedDisbursedAmountsOverApplied setting on Non MultiDisbursement Loan Product + // 1. Create a Loan product with allowApprovedDisbursedAmountsOverApplied in true using overAppliedCalculationType + // flat amount + // 2. Submit Loan with 1,000 + // 3. Approve the Loan with 1,300 + // 4. Disburse the Loan with 1,450 + @Test + public void uc155() { + final String operationDate = "1 January 2024"; + AtomicLong createdLoanId = new AtomicLong(); + runAt(operationDate, () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRatePerPeriod(12.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).numberOfRepayments(4)// + .repaymentEvery(1)// + .repaymentFrequencyType(1L)// + .allowPartialPeriodInterestCalcualtion(false)// + .installmentAmountInMultiplesOf(null)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("flat")// + .overAppliedNumber(500)// + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, 4) + .transactionProcessingStrategyCode("advanced-payment-allocation-strategy")// + .interestRatePerPeriod(BigDecimal.valueOf(12.0)); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(1300)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertTrue(loanDetails.getStatus().getWaitingForDisbursal()); + assertEquals("1300.000000", loanDetails.getApprovedPrincipal().toString()); + + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en") + .transactionAmount(BigDecimal.valueOf(1550.0)))); + + Assertions.assertTrue(callFailedRuntimeException.getMessage() + .contains("Loan disbursal amount can't be greater than maximum applied loan amount calculation")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1450.0))); + + loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals("1450.000000", loanDetails.getSummary().getPrincipalOutstanding().toString()); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");