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 68bfd7a3f83413316044e6f1ce51f18ca0dbe644
Author: mariiaKraievska <[email protected]>
AuthorDate: Fri Jul 25 16:24:06 2025 +0300

    FINERACT-2308: Allow approval / disbursal above loan applied amount for 
multidisbursal progressive loan that does expect tranches
---
 .../test/data/loanproduct/DefaultLoanProduct.java  |   1 +
 .../global/LoanProductGlobalInitializerStep.java   |  37 +++++
 .../fineract/test/support/TestContextKey.java      |   1 +
 .../test/resources/features/LoanProduct.feature    |  14 ++
 .../DelinquencyReadPlatformServiceImpl.java        |   6 +-
 .../portfolio/loanproduct/domain/LoanProduct.java  |   8 --
 .../portfolio/loanproduct.validation.feature       |   2 -
 .../LoanProductOverAppliedAmountTest.java          | 152 +++++++++++++++++++++
 8 files changed, 210 insertions(+), 11 deletions(-)

diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
index 43dbdab2ee..157415377b 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
@@ -132,6 +132,7 @@ public enum DefaultLoanProduct implements LoanProduct {
     LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME, //
     LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES, //
     
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES_CHARGE_OFF_REASON, //
+    
LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES,
 //
     
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC,
 //
     
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME,
 //
     
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE,
 //
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
index afd90b0d4d..d7637bcd92 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
@@ -3426,6 +3426,43 @@ public class LoanProductGlobalInitializerStep implements 
FineractGlobalInitializ
         
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_MULTIDISBURSAL_EXPECTS_TRANCHES,
                 responseLoanProductMultidisbursalExpectTranches);
 
+        // LP2 with progressive loan schedule + horizontal + interest EMI + 
360/30
+        // + interest recalculation, preClosureInterestCalculationStrategy= 
till preclose
+        // Frequency for recalculate Outstanding Principal: Daily, Frequency 
Interval for recalculation: 1
+        // allow approved/disbursed amount over applied amount is enabled with 
percentage
+        // multidisbursal loan that expects tranches
+        // type
+        final String name131 = 
DefaultLoanProduct.LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES
+                .getName();
+        final PostLoanProductsRequest 
loanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches
 = loanProductsRequestFactory
+                .defaultLoanProductsRequestLP2EmiCapitalizedIncome()//
+                .name(name131)//
+                .daysInYearType(DaysInYearType.DAYS360.value)//
+                .daysInMonthType(DaysInMonthType.DAYS30.value)//
+                .isInterestRecalculationEnabled(true)//
+                .preClosureInterestCalculationStrategy(1)//
+                .rescheduleStrategyMethod(4)//
+                .interestRecalculationCompoundingMethod(0)//
+                .recalculationRestFrequencyType(2)//
+                .recalculationRestFrequencyInterval(1)//
+                .paymentAllocation(List.of(//
+                        createPaymentAllocation("DEFAULT", 
"NEXT_INSTALLMENT"), //
+                        createPaymentAllocation("GOODWILL_CREDIT", 
"LAST_INSTALLMENT"), //
+                        createPaymentAllocation("MERCHANT_ISSUED_REFUND", 
"REAMORTIZATION"), //
+                        createPaymentAllocation("PAYOUT_REFUND", 
"NEXT_INSTALLMENT"))) //
+                .allowApprovedDisbursedAmountsOverApplied(true)//
+                
.overAppliedCalculationType(OverAppliedCalculationType.PERCENTAGE.value)//
+                .overAppliedNumber(50)//
+                .multiDisburseLoan(true)//
+                .disallowExpectedDisbursements(false)//
+                .maxTrancheCount(10)//
+                .outstandingLoanBalance(10000.0);//
+        final Response<PostLoanProductsResponse> 
responseLoanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches
 = loanProductsApi
+                
.createLoanProduct(loanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches)
+                .execute();
+        TestContext.INSTANCE.set(
+                
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES,
+                
responseLoanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches);
     }
 
     public static AdvancedPaymentData createPaymentAllocation(String 
transactionType, String futureInstallmentAllocationRule,
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index a70ddc138a..e404e50432 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -255,4 +255,5 @@ public abstract class TestContextKey {
     public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES
 = 
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeAmountPlusPercentageInterestCharges";
     public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES
 = 
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeAllCharges";
     public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE
 = 
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeFlatPlusPercentageInterestChargesMultidisbursal";
+    public static final String 
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES
 = 
"loanProductCreateResponseLP2ProgressiveAdvancedPaymentInterestRecalculationMultidisbursalApprovedOverAppliedAmountExpectedTransches";
 }
diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanProduct.feature 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanProduct.feature
index 5c9efe1440..4808c43a30 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/LoanProduct.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanProduct.feature
@@ -251,3 +251,17 @@ Feature: LoanProduct
     Then Loan Product response contains Buy Down Fees flag "false"
     Then Loan Details response contains Buy Down Fees flag "false"
 
+  @TestRailId:C3884
+  Scenario: As a user I would like to verify multi-disburse loan product with 
over-applied amount and expected tranches can be created
+    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 disbursements 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                | 
700.0           [...]
+    And Admin successfully approves the loan on "1 January 2024" with "1000" 
amount and expected disbursement date on "1 January 2024"
+    And Admin successfully disburse the loan on "1 January 2024" with "1000" 
EUR transaction amount
+    Then Loan status will be "ACTIVE"
+    When Admin sets the business date to "2 January 2024"
+    And Admin successfully disburse the loan on "2 January 2024" with "300" 
EUR transaction amount
+    And Admin adds capitalized income with "AUTOPAY" payment type to the loan 
on "02 January 2024" with "200" EUR transaction amount
+
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 7406584aff..62bbbe2fa1 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
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.delinquency.service;
 import static 
org.apache.fineract.portfolio.loanaccount.domain.Loan.EARLIEST_UNPAID_DATE;
 import static 
org.apache.fineract.portfolio.loanaccount.domain.Loan.NEXT_UNPAID_DUE_DATE;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.Collection;
 import java.util.Comparator;
@@ -148,7 +149,10 @@ public class DelinquencyReadPlatformServiceImpl implements 
DelinquencyReadPlatfo
             // Overpaid
             // loans
             collectionData = 
loanDelinquencyDomainService.getOverdueCollectionData(loan, 
effectiveDelinquencyList);
-            
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
+            // Ensure availableDisbursementAmount is never negative
+            final BigDecimal availableAmount = 
loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount());
+            collectionData
+                    
.setAvailableDisbursementAmount(availableAmount.compareTo(BigDecimal.ZERO) < 0 
? BigDecimal.ZERO : availableAmount);
             
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig,
 loan));
 
             final LoanTransaction lastPayment = 
loan.getLastPaymentTransaction();
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 5f73f1985f..c53c0f6b9a 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
@@ -414,14 +414,6 @@ public class LoanProduct extends 
AbstractPersistableCustom<Long> {
             }
         }
 
-        if (this.allowApprovedDisbursedAmountsOverApplied) {
-            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");
-            }
-        }
-
         if (this.overAppliedCalculationType == null || 
this.overAppliedCalculationType.isEmpty()) {
             if (this.allowApprovedDisbursedAmountsOverApplied) {
                 throw new LoanProductGeneralRuleException(
diff --git 
a/fineract-provider/src/test/resources/features/portfolio/loanproduct.validation.feature
 
b/fineract-provider/src/test/resources/features/portfolio/loanproduct.validation.feature
index 36a7a0f2ba..906b17aa51 100644
--- 
a/fineract-provider/src/test/resources/features/portfolio/loanproduct.validation.feature
+++ 
b/fineract-provider/src/test/resources/features/portfolio/loanproduct.validation.feature
@@ -28,10 +28,8 @@ Feature: Loan Product Validation
     Examples:
       | allowMultipleDisbursal | disallowExpectedDisbursements | 
allowApprovedDisbursedAmountsOverApplied | overAppliedCalculationType | 
overAppliedNumber | exception                                                   
                        | template                                              
                                                   |
       | false                  | true                          | false         
                           | -                          | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.allowMultipleDisbursals.not.set.disallowExpectedDisbursements.cant.be.set
                      |
-      | true                   | false                         | true          
                           | -                          | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set
     |
       | true                   | true                          | true          
                           | -                          | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory
        |
       | true                   | true                          | true          
                           | -                          | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory
        |
-      | true                   | false                         | true          
                           | -                          | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set
     |
       | true                   | true                          | true          
                           | notflat                    | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | error.msg.overAppliedCalculationType.must.be.percentage.or.flat              
                            |
       | true                   | true                          | false         
                           | flat                       | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.allowApprovedDisbursedAmountsOverApplied.is.not.set.overAppliedCalculationType.cant.be.entered
 |
       | true                   | true                          | true          
                           | flat                       | -1                | 
org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException
 | 
error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedNumber.is.mandatory
                 |
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductOverAppliedAmountTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductOverAppliedAmountTest.java
new file mode 100644
index 0000000000..489f70ac95
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductOverAppliedAmountTest.java
@@ -0,0 +1,152 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansDisbursementData;
+import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
+import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.junit.jupiter.api.Test;
+
+public class LoanProductOverAppliedAmountTest extends BaseLoanIntegrationTest {
+
+    @Test
+    public void 
testCreateMultiDisburseLoanProductWithOverAppliedAmountAndExpectedTranches() {
+        runAt("01 January 2024", () -> {
+            // Create Loan Product with multi-disburse, expected tranches, and 
over-applied amount
+            final PostLoanProductsRequest loanProductRequest = 
create4IProgressive().multiDisburseLoan(true).maxTrancheCount(5)
+                    
.outstandingLoanBalance(10000.0).disallowExpectedDisbursements(false) // 
Expected tranches enabled
+                    
.allowApprovedDisbursedAmountsOverApplied(true).overAppliedCalculationType("percentage").overAppliedNumber(50);
+
+            // This should not throw an exception
+            final PostLoanProductsResponse loanProductResponse = 
assertDoesNotThrow(
+                    () -> 
loanProductHelper.createLoanProduct(loanProductRequest));
+
+            assertNotNull(loanProductResponse);
+            assertNotNull(loanProductResponse.getResourceId());
+
+            // Retrieve the created loan product to verify settings
+            final GetLoanProductsProductIdResponse retrievedProduct = 
loanProductHelper
+                    
.retrieveLoanProductById(loanProductResponse.getResourceId());
+
+            // Verify the loan product was created with correct settings
+            assertEquals(true, retrievedProduct.getMultiDisburseLoan());
+            assertEquals(false, 
retrievedProduct.getDisallowExpectedDisbursements());
+            assertEquals(true, 
retrievedProduct.getAllowApprovedDisbursedAmountsOverApplied());
+            assertEquals("percentage", 
retrievedProduct.getOverAppliedCalculationType());
+        });
+    }
+
+    @Test
+    public void 
testModifyMultiDisburseLoanProductWithOverAppliedAmountAndExpectedTranches() {
+        runAt("01 January 2024", () -> {
+            // Create initial loan product without over-applied amount
+            final PostLoanProductsRequest initialLoanProductRequest = 
create4IProgressive().multiDisburseLoan(true).maxTrancheCount(5)
+                    
.outstandingLoanBalance(10000.0).disallowExpectedDisbursements(false).allowApprovedDisbursedAmountsOverApplied(false)
+                    .overAppliedCalculationType(null).overAppliedNumber(null);
+
+            final PostLoanProductsResponse initialLoanProductResponse = 
loanProductHelper.createLoanProduct(initialLoanProductRequest);
+            final Long loanProductId = 
initialLoanProductResponse.getResourceId();
+
+            // Modify loan product to enable over-applied amount
+            final PutLoanProductsProductIdRequest modifyRequest = new 
PutLoanProductsProductIdRequest()
+                    
.allowApprovedDisbursedAmountsOverApplied(true).overAppliedCalculationType("flat").overAppliedNumber(200).locale("en");
+
+            final PutLoanProductsProductIdResponse modifyResponse = 
assertDoesNotThrow(
+                    () -> 
loanProductHelper.updateLoanProductById(loanProductId, modifyRequest));
+
+            assertNotNull(modifyResponse);
+
+            // Retrieve the updated loan product to verify settings
+            final GetLoanProductsProductIdResponse retrievedProduct = 
loanProductHelper.retrieveLoanProductById(loanProductId);
+            assertEquals(true, retrievedProduct.getMultiDisburseLoan());
+            assertEquals(false, 
retrievedProduct.getDisallowExpectedDisbursements());
+            assertEquals(true, 
retrievedProduct.getAllowApprovedDisbursedAmountsOverApplied());
+            assertEquals("flat", 
retrievedProduct.getOverAppliedCalculationType());
+        });
+    }
+
+    @Test
+    public void 
testAvailableDisbursementAmountNotNegativeWhenDisbursedAmountExceedsApprovedAmount()
 {
+        runAt("01 January 2024", () -> {
+            // Create Loan Product with over-applied amount enabled
+            final PostLoanProductsRequest loanProductRequest = 
create4IProgressive().multiDisburseLoan(true).maxTrancheCount(5)
+                    
.outstandingLoanBalance(10000.0).disallowExpectedDisbursements(false) // 
Expected tranches enabled
+                    
.allowApprovedDisbursedAmountsOverApplied(true).overAppliedCalculationType("percentage").overAppliedNumber(50);
+
+            final PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(loanProductRequest);
+            final Long loanProductId = loanProductResponse.getResourceId();
+
+            // Create client
+            final Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            assertNotNull(clientId);
+
+            // Create and approve loan with amount 1000
+            final Long loanId = applyAndApproveProgressiveLoan(clientId, 
loanProductId, "1 January 2024", 1000.0, 7.0, 6,
+                    request -> request.disbursementData(List.of(new 
PostLoansDisbursementData().expectedDisbursementDate("1 January 2024")
+                            .principal(BigDecimal.valueOf(1000.0)))));
+
+            // Disburse loan with amount 1500 (exceeds approved amount, but 
allowed due to over-applied setting)
+            disburseLoan(loanId, BigDecimal.valueOf(1500.0), "01 January 
2024");
+
+            // Verify loan is active
+            verifyLoanStatus(loanId, LoanStatus.ACTIVE);
+
+            final GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertNotNull(loanDetails);
+
+            // Verify that the loan was created and disbursed successfully
+            assert loanDetails.getStatus() != null;
+            assertEquals(Boolean.TRUE, loanDetails.getStatus().getActive());
+
+            // Verify the amounts
+            assert loanDetails.getApprovedPrincipal() != null;
+            assertEquals(BigDecimal.valueOf(1000.0), 
loanDetails.getApprovedPrincipal().setScale(1, RoundingMode.HALF_UP));
+            assert loanDetails.getDisbursementDetails() != null;
+            double disbursementPrincipalSum = 
loanDetails.getDisbursementDetails().stream().mapToDouble(detail -> {
+                assert detail.getPrincipal() != null;
+                return detail.getPrincipal();
+            }).sum();
+            assertEquals(1500.0, disbursementPrincipalSum);
+
+            // The key test: availableDisbursementAmount should be 0 (not 
negative)
+            // since disbursed amount (1500) > approved amount (1000)
+            assert loanDetails.getDelinquent() != null;
+            final BigDecimal availableDisbursementAmount = 
loanDetails.getDelinquent().getAvailableDisbursementAmount();
+            assertNotNull(availableDisbursementAmount);
+            assertTrue(availableDisbursementAmount.compareTo(BigDecimal.ZERO) 
>= 0,
+                    "availableDisbursementAmount should not be negative. 
Expected >= 0, but was: " + availableDisbursementAmount);
+            assertEquals(BigDecimal.ZERO, availableDisbursementAmount,
+                    "availableDisbursementAmount should be 0 when disbursed 
amount exceeds approved amount");
+        });
+    }
+}

Reply via email to