This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 45dbe77fe3af8bb432b8cc928552c2647fada33f Author: mariiaKraievska <[email protected]> AuthorDate: Fri Mar 6 10:20:04 2026 +0200 FINERACT-2522: Max disb amount validation issue in case multidisb loan with expected tranches and overApplied enabled --- .../fineract/test/helper/ErrorMessageHelper.java | 4 ++ .../fineract/test/stepdef/loan/LoanStepDef.java | 13 ++++ .../test/resources/features/EMICalculation.feature | 4 +- .../src/test/resources/features/Loan.feature | 70 +++++++++++++++++++++- .../serialization/LoanDisbursementValidator.java | 20 +++---- .../LoanTransactionValidatorImpl.java | 11 ++-- .../service/LoanDisbursementService.java | 2 +- .../ClientLoanIntegrationTest.java | 4 +- 8 files changed, 103 insertions(+), 25 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java index d689a359f2..387d57bcff 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java @@ -128,6 +128,10 @@ public final class ErrorMessageHelper { return "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state."; } + public static String disburseIsNotAllowedExceedApprovedAmountFailure() { + return "Loan can't be disbursed, disburse amount is exceeding approved principal."; + } + public static String loanSubmitDateInFutureFailureMsg() { return "The date on which a loan is submitted cannot be in the future."; } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java index b682f3ea69..ca3bd43b50 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java @@ -1863,6 +1863,19 @@ public class LoanStepDef extends AbstractStepDef { assertThat(exception.getDeveloperMessage()).contains(ErrorMessageHelper.disburseIsNotAllowedFailure()); } + @Then("Admin fails to disburse the loan on {string} with {string} amount due to exceed approved amount") + public void disburseIsNotAllowedExceedApprovedAmountFailure(String disbursementDate, String disbursementAmount) { + final PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + final long loanId = loanResponse.getLoanId(); + final PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest() + .actualDisbursementDate(disbursementDate).transactionAmount(new BigDecimal(disbursementAmount)); + + final CallFailedRuntimeException exception = fail( + () -> fineractClient.loans().stateTransitions(loanId, disburseRequest, Map.of("command", "disburse"))); + assertThat(exception.getStatus()).as(ErrorMessageHelper.dateFailureErrorCodeMsg()).isEqualTo(403); + assertThat(exception.getDeveloperMessage()).contains(ErrorMessageHelper.disburseIsNotAllowedExceedApprovedAmountFailure()); + } + @Then("Admin fails to disburse the loan on {string} with {string} EUR transaction amount because of charge-off that was performed for the loan") public void disburseChargedOffLoanFailure(String actualDisbursementDate, String transactionAmount) { PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature index e50ec3ada9..cf3a1e160c 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature @@ -7950,7 +7950,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 30 January 2025 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | | 31 January 2025 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | | 01 February 2025 | Disbursement | 300.0 | 0.0 | 0.0 | 0.0 | 0.0 | 882.78 | false | false | - Then Admin fails to disburse the loan on "01 February 2025" with "100" amount + Then Admin fails to disburse the loan on "01 February 2025" with "100" amount due to exceed approved amount # --- undo last disbursement --- # When Admin successfully undo last disbursal Then Loan Tranche Details tab has the following data: @@ -8141,7 +8141,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 31 January 2025 | Repayment | 117.86 | 116.16 | 1.7 | 0.0 | 0.0 | 468.02 | false | false | | 31 January 2025 | Accrual | 0.11 | 0.0 | 0.11 | 0.0 | 0.0 | 0.0 | false | false | | 01 February 2025 | Disbursement | 300.0 | 0.0 | 0.0 | 0.0 | 0.0 | 768.02 | false | false | - Then Admin fails to disburse the loan on "01 February 2025" with "100" amount + Then Admin fails to disburse the loan on "01 February 2025" with "100" amount due to exceed approved amount # --- undo disbursement --- # When Admin sets the business date to "02 February 2025" When Admin runs inline COB job for Loan diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index fbcc73ade9..9b03c69436 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -151,13 +151,13 @@ Feature: Loan And Admin checks available disbursement amount 0.0 EUR Then Loan has availableDisbursementAmountWithOverApplied field with value: 300 Then Admin fails to disburse the loan on "2 January 2024" with "1600" EUR transaction amount because of wrong amount - And Admin successfully disburse the loan on "2 January 2024" with "1500" EUR transaction amount + And Admin successfully disburse the loan on "2 January 2024" with "1300" EUR transaction amount Then Loan has availableDisbursementAmountWithOverApplied field with value: 0 Then Loan status will be "ACTIVE" And Admin checks available disbursement amount 0.0 EUR Then Loan Tranche Details tab has the following data: | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | - | 01 January 2024 | 02 January 2024 | 1500.0 | | + | 01 January 2024 | 02 January 2024 | 1300.0 | | | 05 January 2024 | | 200.0 | 1200.0 | When Loan Pay-off is made on "2 January 2024" @@ -8335,7 +8335,7 @@ Feature: Loan | 01 January 2025 | Disbursement | 700.0 | 0.0 | 0.0 | 0.0 | 0.0 | 700.0 | false | false | | 01 January 2025 | Repayment | 117.86 | 117.86 | 0.0 | 0.0 | 0.0 | 582.14 | false | false | | 01 January 2025 | Disbursement | 300.0 | 0.0 | 0.0 | 0.0 | 0.0 | 882.14 | false | false | - Then Admin fails to disburse the loan on "01 January 2025" with "100" amount + Then Admin fails to disburse the loan on "01 January 2025" with "100" amount due to exceed approved amount # --- undo disbursement --- # When Admin successfully undo last disbursal Then Loan Tranche Details tab has the following data: @@ -9094,3 +9094,67 @@ Feature: Loan Then LoanDisbursalTransactionBusinessEvent has changedTerms "false" When Loan Pay-off is made on "08 January 2024" Then Loan is closed with zero outstanding balance and it's all installments have obligations met + + @TestRailId:C70224 + Scenario: Verify max disb amount validation in case multidisb loan that expect tranches with overapplied setting enabled - UC1 + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When Admin creates a fully customized loan with disbursement details and following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | 1st_tranche_disb_expected_date |1st_tranche_disb [...] + | LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES | 01 January 2024 | 1000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 1000.0 [...] + And Admin successfully approves the loan on "1 January 2024" with "1200" amount and expected disbursement date on "1 January 2024" + Then Loan has availableDisbursementAmountWithOverApplied field with value: 500 + Then Loan status will be "APPROVED" + Then Loan Tranche Details tab has the following data: + | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | + | 01 January 2024 | | 1000.0 | | + And Admin successfully add disbursement detail to the loan on "5 January 2024" with 200 EUR transaction amount + Then Loan Tranche Details tab has the following data: + | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | + | 01 January 2024 | | 1000.0 | | + | 05 January 2024 | | 200.0 | 1200.0 | + Then Loan has availableDisbursementAmountWithOverApplied field with value: 300 + Then Admin fails to disburse the loan on "1 January 2024" with "1600" EUR transaction amount because of wrong amount + Then Admin fails to disburse the loan on "1 January 2024" with "1500" EUR transaction amount because of wrong amount + And Admin successfully disburse the loan on "1 January 2024" with "1300" EUR transaction amount + Then Loan has availableDisbursementAmountWithOverApplied field with value: 0 + Then Loan status will be "ACTIVE" + Then Loan Tranche Details tab has the following data: + | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | + | 01 January 2024 | 01 January 2024 | 1300.0 | | + | 05 January 2024 | | 200.0 | 1200.0 | + When Admin sets the business date to "5 January 2024" + Then Admin fails to disburse the loan on "5 January 2024" with "300" EUR transaction amount because of wrong amount + And Admin successfully disburse the loan on "5 January 2024" with "200" EUR transaction amount + + @TestRailId:C70225 + Scenario: Verify max disb amount validation in case multidisb loan that expect tranches with overapplied setting enabled - UC2 + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + When Admin creates a fully customized loan with disbursement details and following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | 1st_tranche_disb_expected_date |1st_tranche_disb [...] + | LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES | 01 January 2024 | 1000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 1000.0 [...] + And Admin successfully approves the loan on "1 January 2024" with "1200" amount and expected disbursement date on "1 January 2024" + Then Loan has availableDisbursementAmountWithOverApplied field with value: 500 + Then Loan status will be "APPROVED" + Then Loan Tranche Details tab has the following data: + | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | + | 01 January 2024 | | 1000.0 | | + And Admin successfully add disbursement detail to the loan on "5 January 2024" with 200 EUR transaction amount + Then Loan Tranche Details tab has the following data: + | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | + | 01 January 2024 | | 1000.0 | | + | 05 January 2024 | | 200.0 | 1200.0 | + And Admin checks available disbursement amount 0.0 EUR + Then Loan has availableDisbursementAmountWithOverApplied field with value: 300 + Then Admin fails to disburse the loan on "1 January 2024" with "1600" EUR transaction amount because of wrong amount + And Admin successfully disburse the loan on "1 January 2024" with "1100" EUR transaction amount + Then Loan status will be "ACTIVE" + Then Loan has availableDisbursementAmountWithOverApplied field with value: 200 + Then Loan Tranche Details tab has the following data: + | Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount | + | 01 January 2024 | 01 January 2024 | 1100.0 | | + | 05 January 2024 | | 200.0 | 1200.0 | + When Admin sets the business date to "5 January 2024" + Then Admin fails to disburse the loan on "5 January 2024" with "800" EUR transaction amount because of wrong amount + And Admin successfully disburse the loan on "5 January 2024" with "400" EUR transaction amount 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 fe2fa363e7..df3f4558a7 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 @@ -34,24 +34,20 @@ public final class LoanDisbursementValidator { private final LoanApplicationValidator loanApplicationValidator; - public void compareDisbursedToApprovedOrProposedPrincipal(final Loan loan, final BigDecimal disbursedAmount, - final BigDecimal totalDisbursed) { + public void compareDisbursedToApprovedOrProposedPrincipal(final Loan loan, final BigDecimal totalDisbursed) { 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()) { + if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + // Validate total disbursed amount (after this transaction) against max allowed validateOverMaximumAmount(loan, totalDisbursed, netCapitalizedIncome); } else { - if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - validateOverMaximumAmount(loan, disbursedAmount, netCapitalizedIncome); - } else { - if ((totalDisbursed.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()); - } + if ((totalDisbursed.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()); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java index 65c55ee8c4..43132cc328 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidatorImpl.java @@ -162,8 +162,9 @@ public class LoanTransactionValidatorImpl implements LoanTransactionValidator { validateLoanClientIsActive(loan); validateLoanGroupIsActive(loan); - final BigDecimal disbursedAmount = loan.getSummary().getTotalPrincipalDisbursed(); - loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, principal, disbursedAmount); + final BigDecimal totalDisbursedAmount = principal != null ? loan.getDisbursedAmount().add(principal) + : loan.getDisbursedAmount(); + loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, totalDisbursedAmount); if (loan.isChargedOff()) { throw new GeneralPlatformDomainRuleException("error.msg.loan.disbursal.not.allowed.on.charged.off", @@ -186,9 +187,9 @@ public class LoanTransactionValidatorImpl implements LoanTransactionValidator { if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) && loan.getLoanType().isIndividualAccount()) { BigDecimal totalCollateral = collectTotalCollateral(loanCollateralManagements); - // Validate the loan collateral value against the disbursedAmount - if (disbursedAmount.compareTo(totalCollateral) > 0) { - throw new LoanCollateralAmountNotSufficientException(disbursedAmount); + // Validate the loan collateral value against the total disbursed amount after this transaction + if (totalDisbursedAmount.compareTo(totalCollateral) > 0) { + throw new LoanCollateralAmountNotSufficientException(totalDisbursedAmount); } } 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 b8fe98fa56..0d6d0296aa 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 @@ -208,7 +208,7 @@ public class LoanDisbursementService { .setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount()); totalAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount(); } - loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount); + loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, totalAmount); } return disburseAmount; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index fa6e570489..c8208b4cb1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -6413,7 +6413,7 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest { .locale("en").dateFormat(DATETIME_PATTERN)); }); assertEquals(403, exception.getResponse().code()); - assertTrue(exception.getMessage().contains("error.msg.loan.disbursal.not.allowed.on.charged.off")); + assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation")); exception = assertThrows(CallFailedRuntimeException.class, () -> { errorLoanTransactionHelper.disburseLoan((long) loanID, @@ -6421,7 +6421,7 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest { .locale("en").dateFormat(DATETIME_PATTERN)); }); assertEquals(403, exception.getResponse().code()); - assertTrue(exception.getMessage().contains("error.msg.loan.disbursal.not.allowed.on.charged.off")); + assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation")); LOAN_TRANSACTION_HELPER.makeLoanRepayment((long) loanID, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) .transactionDate("07 September 2022").locale("en").transactionAmount(5000.0));
