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));

Reply via email to