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 6d65f0792484e2413de9d4e10bdbff8d1ebfbb0a
Author: mark.vituska <[email protected]>
AuthorDate: Tue Jul 1 13:08:05 2025 +0200

    FINERACT-2317: Add support for modifying loan approved amounts with 
validation and history tracking
---
 .../commands/service/CommandWrapperBuilder.java    |   9 +
 .../portfolio/common/service/Validator.java        |  23 +-
 .../fineract/test/helper/ErrorMessageHelper.java   |  10 +-
 .../apache/fineract/test/helper/ErrorResponse.java |   9 +
 .../fineract/test/stepdef/loan/LoanStepDef.java    |  98 +++++
 .../LoanApprovedAmountChangedBusinessEvent.java    |  35 ++
 .../data/LoanApprovedAmountHistoryData.java        |  47 ++
 .../domain/LoanApprovedAmountHistory.java          |  47 ++
 .../LoanApprovedAmountHistoryRepository.java       |  39 ++
 ...anApprovedAmountModificationCommandHandler.java |  42 ++
 .../serialization/LoanApprovedAmountValidator.java |  26 ++
 .../LoanApprovedAmountWritePlatformService.java    |  27 ++
 ...LoanApprovedAmountWritePlatformServiceImpl.java |  76 ++++
 .../module/fineract-loan/persistence.xml           |   1 +
 .../loanaccount/api/LoansApiResource.java          |  69 +++
 .../loanaccount/api/LoansApiResourceSwagger.java   |  43 ++
 .../serialization/LoanApplicationValidator.java    |  25 +-
 .../LoanApprovedAmountValidatorImpl.java           | 104 +++++
 .../serialization/LoanDisbursementValidator.java   |   2 +-
 .../LoanTransactionValidatorImpl.java              |   2 +-
 .../service/LoanDisbursementService.java           |   1 +
 .../starter/LoanAccountConfiguration.java          |   2 +-
 .../db/changelog/tenant/changelog-tenant.xml       |   2 +
 ..._add_LoanApprovedAmountChangedBusinessEvent.xml |  31 ++
 .../0192_create_loan_approved_amount_history.xml   |  79 ++++
 ...nalEventConfigurationValidationServiceTest.java |   4 +-
 .../integrationtests/BaseLoanIntegrationTest.java  |  12 +
 .../LoanModifyApprovedAmountTests.java             | 481 +++++++++++++++++++++
 .../common/ExternalEventConfigurationHelper.java   |   5 +
 .../common/loans/LoanTestLifecycleExtension.java   |   9 +
 30 files changed, 1348 insertions(+), 12 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
index a3f2c14c4b..42f23b6bac 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java
@@ -3876,4 +3876,13 @@ public class CommandWrapperBuilder {
         this.href = "/loans/" + loanId + 
"/transactions/template?command=buyDownFee";
         return this;
     }
+
+    public CommandWrapperBuilder updateLoanApprovedAmount(final Long loanId) {
+        this.actionName = "UPDATE";
+        this.entityName = "LOAN_APPROVED_AMOUNT";
+        this.entityId = loanId;
+        this.loanId = loanId;
+        this.href = "/loans/" + loanId;
+        return this;
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java
 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java
index eb1f3237f4..c953bb545b 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/Validator.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.function.Consumer;
 import org.apache.fineract.infrastructure.core.data.ApiParameterError;
 import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import 
org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
 import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
 
 public final class Validator {
@@ -30,14 +31,28 @@ public final class Validator {
     private Validator() {}
 
     public static void validateOrThrow(String resource, 
Consumer<DataValidatorBuilder> baseDataValidator) {
-        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
-        final DataValidatorBuilder dataValidatorBuilder = new 
DataValidatorBuilder(dataValidationErrors).resource(resource);
-
-        baseDataValidator.accept(dataValidatorBuilder);
+        final List<ApiParameterError> dataValidationErrors = 
getApiParameterErrors(resource, baseDataValidator);
 
         if (!dataValidationErrors.isEmpty()) {
             throw new 
PlatformApiDataValidationException("validation.msg.validation.errors.exist", 
"Validation errors exist.",
                     dataValidationErrors);
         }
     }
+
+    public static void validateOrThrowDomainViolation(String resource, 
Consumer<DataValidatorBuilder> baseDataValidator) {
+        final List<ApiParameterError> dataValidationErrors = 
getApiParameterErrors(resource, baseDataValidator);
+
+        if (!dataValidationErrors.isEmpty()) {
+            throw new 
GeneralPlatformDomainRuleException("validation.msg.validation.errors.exist", 
"Validation errors exist.",
+                    dataValidationErrors.toArray(new Object[0]));
+        }
+    }
+
+    private static List<ApiParameterError> getApiParameterErrors(String 
resource, Consumer<DataValidatorBuilder> baseDataValidator) {
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        final DataValidatorBuilder dataValidatorBuilder = new 
DataValidatorBuilder(dataValidationErrors).resource(resource);
+
+        baseDataValidator.accept(dataValidatorBuilder);
+        return dataValidationErrors;
+    }
 }
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 931495f4a4..912db6b868 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
@@ -65,7 +65,7 @@ public final class ErrorMessageHelper {
     }
 
     public static String addDisbursementExceedApprovedAmountFailure() {
-        return "Loan can't be disbursed,disburse amount is exceeding approved 
principal ";
+        return "Loan can't be disbursed, disburse amount is exceeding approved 
principal.";
     }
 
     public static String addDisbursementExceedMaxAppliedAmountFailure(String 
totalDisbAmount, String maxDisbursalAmount) {
@@ -980,4 +980,12 @@ public final class ErrorMessageHelper {
     public static String addInstallmentFeePrincipalPercentageChargeFailure() {
         return "Failed data validation due to: 
installment.loancharge.with.calculation.type.principal.not.allowed.";
     }
+
+    public static String updateApprovedLoanExceedPrincipalFailure() {
+        return "Failed data validation due to: 
less.than.disbursed.principal.and.capitalized.income.";
+    }
+
+    public static String updateApprovedLoanLessMinAllowedAmountFailure() {
+        return "The parameter `amount` must be greater than 0.";
+    }
 }
diff --git 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorResponse.java
 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorResponse.java
index db4e7c3765..f746d56a37 100644
--- 
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorResponse.java
+++ 
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorResponse.java
@@ -61,5 +61,14 @@ public class ErrorResponse {
     public static class Error {
 
         private String developerMessage;
+        private List<ErrorMessageArg> args;
+    }
+
+    @NoArgsConstructor
+    @Getter
+    @Setter
+    public static class ErrorMessageArg {
+
+        private Object value;
     }
 }
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 348287030b..5f451dc347 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
@@ -24,6 +24,7 @@ import static 
org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_A
 import static 
org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR;
 import static 
org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_ADV_PYMNT_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR;
 import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.CHARGE_OFF_REASONS;
+import static 
org.apache.fineract.test.factory.LoanProductsRequestFactory.LOCALE_EN;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -98,6 +99,8 @@ import 
org.apache.fineract.client.models.PostLoansRequestChargeData;
 import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
 import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
+import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
+import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
 import org.apache.fineract.client.models.PutLoansLoanIdRequest;
 import org.apache.fineract.client.models.PutLoansLoanIdResponse;
 import org.apache.fineract.client.services.BusinessDateManagementApi;
@@ -574,6 +577,12 @@ public class LoanStepDef extends AbstractStepDef {
         
createFullyCustomizedLoanWithExpectedTrancheDisbursementsDetails(data.get(1));
     }
 
+    @When("Admin creates a fully customized loan with three expected 
disbursements details and following data:")
+    public void createFullyCustomizedLoanWithThreeDisbursementsDetails(final 
DataTable table) throws IOException {
+        final List<List<String>> data = table.asLists();
+        
createFullyCustomizedLoanWithThreeExpectedTrancheDisbursementsDetails(data.get(1));
+    }
+
     @When("Admin creates a fully customized loan with forced disabled 
downpayment with the following data:")
     public void 
createFullyCustomizedLoanWithForcedDisabledDownpayment(DataTable table) throws 
IOException {
         List<List<String>> data = table.asLists();
@@ -3688,6 +3697,27 @@ public class LoanStepDef extends AbstractStepDef {
         createFullyCustomizedLoanExpectsTrancheDisbursementDetails(loanData, 
disbursementDetail);
     }
 
+    public void 
createFullyCustomizedLoanWithThreeExpectedTrancheDisbursementsDetails(final 
List<String> loanData) throws IOException {
+        final String expectedDisbursementDateFirstDisbursal = loanData.get(16);
+        final Double disbursementPrincipalAmountFirstDisbursal = 
Double.valueOf(loanData.get(17));
+
+        final String expectedDisbursementDateSecondDisbursal = 
loanData.get(18);
+        final Double disbursementPrincipalAmountSecondDisbursal = 
Double.valueOf(loanData.get(19));
+
+        final String expectedDisbursementDateThirdDisbursal = loanData.get(20);
+        final Double disbursementPrincipalAmountThirdDisbursal = 
Double.valueOf(loanData.get(21));
+
+        List<PostLoansDisbursementData> disbursementDetail = new ArrayList<>();
+        disbursementDetail.add(new 
PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateFirstDisbursal)
+                
.principal(BigDecimal.valueOf(disbursementPrincipalAmountFirstDisbursal)));
+        disbursementDetail.add(new 
PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateSecondDisbursal)
+                
.principal(BigDecimal.valueOf(disbursementPrincipalAmountSecondDisbursal)));
+        disbursementDetail.add(new 
PostLoansDisbursementData().expectedDisbursementDate(expectedDisbursementDateThirdDisbursal)
+                
.principal(BigDecimal.valueOf(disbursementPrincipalAmountThirdDisbursal)));
+
+        createFullyCustomizedLoanExpectsTrancheDisbursementDetails(loanData, 
disbursementDetail);
+    }
+
     public void 
createFullyCustomizedLoanExpectsTrancheDisbursementDetails(final List<String> 
loanData,
             List<PostLoansDisbursementData> disbursementDetail) throws 
IOException {
         final String loanProduct = loanData.get(0);
@@ -4859,4 +4889,72 @@ public class LoanStepDef extends AbstractStepDef {
         log.debug("BuyDown Fee Adjustment created: Transaction ID {}", 
adjustmentResponse.body().getResourceId());
     }
 
+    @Then("Update loan approved amount with new amount {string} value")
+    public void updateLoanApprovedAmount(final String amount) throws 
IOException {
+        final Response<PostLoansResponse> loanResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        final long loanId = loanResponse.body().getLoanId();
+        final Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        final PutLoansApprovedAmountRequest modifyLoanApprovedAmountRequest = 
new PutLoansApprovedAmountRequest().locale(LOCALE_EN)
+                .amount(new BigDecimal(amount));
+
+        final Response<PutLoansApprovedAmountResponse> 
modifyLoanApprovedAmountResponse = loansApi
+                .modifyLoanApprovedAmount(loanId, 
modifyLoanApprovedAmountRequest).execute();
+
+        ErrorHelper.checkSuccessfulApiCall(modifyLoanApprovedAmountResponse);
+
+    }
+
+    @Then("Update loan approved amount is forbidden with amount {string} due 
to exceed applied amount")
+    public void updateLoanApprovedAmountForbiddenExceedAppliedAmount(final 
String amount) throws IOException {
+        final Response<PostLoansResponse> loanResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        final long loanId = loanResponse.body().getLoanId();
+        final Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        final PutLoansApprovedAmountRequest modifyLoanApprovedAmountRequest = 
new PutLoansApprovedAmountRequest().locale(LOCALE_EN)
+                .amount(new BigDecimal(amount));
+
+        final Response<PutLoansApprovedAmountResponse> 
modifyLoanApprovedAmountResponse = loansApi
+                .modifyLoanApprovedAmount(loanId, 
modifyLoanApprovedAmountRequest).execute();
+
+        ErrorResponse errorDetails = 
ErrorResponse.from(modifyLoanApprovedAmountResponse);
+        assertThat(errorDetails.getHttpStatusCode()).isEqualTo(403);
+
+        Object errorArgs = 
errorDetails.getErrors().getFirst().getArgs().getFirst().getValue();
+        String developerMessage;
+        if (errorArgs instanceof Map errorArgsMap) {
+            developerMessage = (String) errorArgsMap.get("developerMessage");
+        } else {
+            developerMessage = errorDetails.getDeveloperMessage();
+        }
+        
assertThat(developerMessage).isEqualTo(ErrorMessageHelper.updateApprovedLoanExceedPrincipalFailure());
+    }
+
+    @Then("Update loan approved amount is forbidden with amount {string} due 
to min allowed amount")
+    public void updateLoanApprovedAmountForbiddenMinAllowedAmount(final String 
amount) throws IOException {
+        final Response<PostLoansResponse> loanResponse = 
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        final long loanId = loanResponse.body().getLoanId();
+        final Response<GetLoansLoanIdResponse> loanDetailsResponse = 
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        final PutLoansApprovedAmountRequest modifyLoanApprovedAmountRequest = 
new PutLoansApprovedAmountRequest().locale(LOCALE_EN)
+                .amount(new BigDecimal(amount));
+
+        final Response<PutLoansApprovedAmountResponse> 
modifyLoanApprovedAmountResponse = loansApi
+                .modifyLoanApprovedAmount(loanId, 
modifyLoanApprovedAmountRequest).execute();
+
+        ErrorResponse errorDetails = 
ErrorResponse.from(modifyLoanApprovedAmountResponse);
+        assertThat(errorDetails.getHttpStatusCode()).isEqualTo(400);
+
+        Object errorArgs = 
errorDetails.getErrors().getFirst().getArgs().getFirst().getValue();
+        String developerMessage;
+        if (errorArgs instanceof Map errorArgsMap) {
+            developerMessage = (String) errorArgsMap.get("developerMessage");
+        } else {
+            developerMessage = errorDetails.getDeveloperMessage();
+        }
+        
assertThat(developerMessage).isEqualTo(ErrorMessageHelper.updateApprovedLoanLessMinAllowedAmountFailure());
+    }
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanApprovedAmountChangedBusinessEvent.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanApprovedAmountChangedBusinessEvent.java
new file mode 100644
index 0000000000..c05cf96f6d
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanApprovedAmountChangedBusinessEvent.java
@@ -0,0 +1,35 @@
+/**
+ * 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.infrastructure.event.business.domain.loan;
+
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+
+public class LoanApprovedAmountChangedBusinessEvent extends LoanBusinessEvent {
+
+    private static final String TYPE = 
"LoanApprovedAmountChangedBusinessEvent";
+
+    public LoanApprovedAmountChangedBusinessEvent(Loan value) {
+        super(value);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApprovedAmountHistoryData.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApprovedAmountHistoryData.java
new file mode 100644
index 0000000000..8598d6ea8d
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanApprovedAmountHistoryData.java
@@ -0,0 +1,47 @@
+/**
+ * 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.portfolio.loanaccount.data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+
+/**
+ * Immutable object representing an Approved Amount change operation on a Loan
+ *
+ * Note: no getter/setters required as google-gson will produce json from 
fields of object.
+ */
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Accessors(chain = true)
+public class LoanApprovedAmountHistoryData implements Serializable {
+
+    private Long loanId;
+    private ExternalId externalLoanId;
+    private BigDecimal newApprovedAmount;
+    private BigDecimal oldApprovedAmount;
+    private OffsetDateTime dateOfChange;
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanApprovedAmountHistory.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanApprovedAmountHistory.java
new file mode 100644
index 0000000000..8e4963b3b0
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanApprovedAmountHistory.java
@@ -0,0 +1,47 @@
+/**
+ * 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.portfolio.loanaccount.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import java.math.BigDecimal;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import 
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
+
+@Entity
+@Table(name = "m_loan_approved_amount_history")
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoanApprovedAmountHistory extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
+
+    @Column(name = "loan_id", nullable = false)
+    private Long loanId;
+
+    @Column(name = "new_approved_amount", scale = 6, precision = 19, nullable 
= false)
+    private BigDecimal newApprovedAmount;
+
+    @Column(name = "old_approved_amount", scale = 6, precision = 19, nullable 
= false)
+    private BigDecimal oldApprovedAmount;
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanApprovedAmountHistoryRepository.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanApprovedAmountHistoryRepository.java
new file mode 100644
index 0000000000..7175b0633c
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanApprovedAmountHistoryRepository.java
@@ -0,0 +1,39 @@
+/**
+ * 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.portfolio.loanaccount.domain;
+
+import java.util.List;
+import 
org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+
+public interface LoanApprovedAmountHistoryRepository
+        extends JpaRepository<LoanApprovedAmountHistory, Long>, 
JpaSpecificationExecutor<LoanApprovedAmountHistory> {
+
+    @Query("""
+            SELECT NEW 
org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData(
+                        laah.loanId, l.externalId, laah.newApprovedAmount, 
laah.oldApprovedAmount, laah.createdDate
+            )
+            FROM LoanApprovedAmountHistory laah JOIN Loan l on laah.loanId = 
l.id
+            WHERE laah.loanId = :loanId
+            """)
+    List<LoanApprovedAmountHistoryData> findAllByLoanId(Long loanId, Pageable 
pageable);
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java
new file mode 100644
index 0000000000..1be514b868
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java
@@ -0,0 +1,42 @@
+/**
+ * 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.portfolio.loanaccount.handler;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.commands.annotation.CommandType;
+import org.apache.fineract.commands.handler.NewCommandSourceHandler;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanApprovedAmountWritePlatformService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@CommandType(entity = "LOAN_APPROVED_AMOUNT", action = "UPDATE")
+public class LoanApprovedAmountModificationCommandHandler implements 
NewCommandSourceHandler {
+
+    private final LoanApprovedAmountWritePlatformService 
loanApprovedAmountWritePlatformService;
+
+    @Override
+    @Transactional
+    public CommandProcessingResult processCommand(JsonCommand command) {
+        return 
loanApprovedAmountWritePlatformService.modifyLoanApprovedAmount(command.getLoanId(),
 command);
+    }
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java
new file mode 100644
index 0000000000..0d61d0805a
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidator.java
@@ -0,0 +1,26 @@
+/**
+ * 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.portfolio.loanaccount.serialization;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+
+public interface LoanApprovedAmountValidator {
+
+    void validateLoanApprovedAmountModification(JsonCommand command);
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java
new file mode 100644
index 0000000000..5fb2621723
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformService.java
@@ -0,0 +1,27 @@
+/**
+ * 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.portfolio.loanaccount.service;
+
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+
+public interface LoanApprovedAmountWritePlatformService {
+
+    CommandProcessingResult modifyLoanApprovedAmount(Long loanId, JsonCommand 
command);
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java
new file mode 100644
index 0000000000..49b01a7f3f
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApprovedAmountWritePlatformServiceImpl.java
@@ -0,0 +1,76 @@
+/**
+ * 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.portfolio.loanaccount.service;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import 
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanApprovedAmountChangedBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository;
+import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanApprovedAmountValidator;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class LoanApprovedAmountWritePlatformServiceImpl implements 
LoanApprovedAmountWritePlatformService {
+
+    private final LoanAssembler loanAssembler;
+    private final LoanApprovedAmountValidator loanApprovedAmountValidator;
+    private final LoanApprovedAmountHistoryRepository 
loanApprovedAmountHistoryRepository;
+    private final BusinessEventNotifierService businessEventNotifierService;
+
+    @Override
+    public CommandProcessingResult modifyLoanApprovedAmount(final Long loanId, 
final JsonCommand command) {
+        // API rule validations
+        
this.loanApprovedAmountValidator.validateLoanApprovedAmountModification(command);
+
+        final Map<String, Object> changes = new LinkedHashMap<>();
+        changes.put("newApprovedAmount", 
command.stringValueOfParameterNamed(LoanApiConstants.amountParameterName));
+        changes.put("locale", command.locale());
+
+        Loan loan = this.loanAssembler.assembleFrom(loanId);
+        changes.put("oldApprovedAmount", loan.getApprovedPrincipal());
+
+        BigDecimal newApprovedAmount = 
command.bigDecimalValueOfParameterNamed(LoanApiConstants.amountParameterName);
+
+        LoanApprovedAmountHistory loanApprovedAmountHistory = new 
LoanApprovedAmountHistory(loan.getId(), newApprovedAmount,
+                loan.getApprovedPrincipal());
+
+        loan.setApprovedPrincipal(newApprovedAmount);
+        
loanApprovedAmountHistoryRepository.saveAndFlush(loanApprovedAmountHistory);
+
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanApprovedAmountChangedBusinessEvent(loan));
+        return new 
CommandProcessingResultBuilder().withCommandId(command.commandId()) //
+                .withEntityId(loan.getId()) //
+                .withEntityExternalId(loan.getExternalId()) //
+                .withOfficeId(loan.getOfficeId()) //
+                .withClientId(loan.getClientId()) //
+                .withGroupId(loan.getGroupId()) //
+                .with(changes) //
+                .build();
+    }
+}
diff --git 
a/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
 
b/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
index 0a8b7e2fec..d749153bed 100644
--- 
a/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
+++ 
b/fineract-loan/src/main/resources/jpa/static-weaving/module/fineract-loan/persistence.xml
@@ -86,6 +86,7 @@
         
<class>org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag</class>
         
<class>org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount</class>
         <class>org.apache.fineract.portfolio.loanaccount.domain.Loan</class>
+        
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistory</class>
         
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCharge</class>
         
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy</class>
         
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement</class>
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
index f5beeeacee..f4f0e51b92 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java
@@ -127,12 +127,14 @@ import 
org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
 import org.apache.fineract.portfolio.loanaccount.data.GlimRepaymentTemplate;
 import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
+import 
org.apache.fineract.portfolio.loanaccount.data.LoanApprovedAmountHistoryData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
 import 
org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
 import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
 import 
org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanApprovedAmountHistoryRepository;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
@@ -176,6 +178,8 @@ import 
org.apache.fineract.portfolio.savings.DepositAccountType;
 import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType;
 import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
 import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 
@@ -303,6 +307,7 @@ public class LoansApiResource {
     private final LoanTermVariationsRepository loanTermVariationsRepository;
     private final LoanSummaryProviderDelegate loanSummaryProviderDelegate;
     private final LoanCapitalizedIncomeBalanceRepository 
loanCapitalizedIncomeBalanceRepository;
+    private final LoanApprovedAmountHistoryRepository 
loanApprovedAmountHistoryRepository;
 
     /*
      * This template API is used for loan approval, ideally this should be 
invoked on loan that are pending for
@@ -872,6 +877,55 @@ public class LoansApiResource {
         return createLoanDelinquencyAction(null, 
ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
     }
 
+    @PUT
+    @Path("{loanId}/approved-amount")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Operation(summary = "Modifies the approved amount of the loan", 
description = "")
+    @RequestBody(required = true, content = @Content(schema = 
@Schema(implementation = 
LoansApiResourceSwagger.PutLoansApprovedAmountRequest.class)))
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
LoansApiResourceSwagger.PutLoansApprovedAmountResponse.class))) })
+    public CommandProcessingResult modifyLoanApprovedAmount(
+            @PathParam("loanId") @Parameter(description = "loanId", required = 
true) final Long loanId, @Context final UriInfo uriInfo,
+            @Parameter(hidden = true) final String apiRequestBodyAsJson) {
+        return modifyLoanApprovedAmount(loanId, ExternalId.empty(), 
apiRequestBodyAsJson);
+    }
+
+    @PUT
+    @Path("external-id/{loanExternalId}/approved-amount")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Operation(summary = "Modifies the approved amount of the loan", 
description = "")
+    @RequestBody(required = true, content = @Content(schema = 
@Schema(implementation = 
LoansApiResourceSwagger.PutLoansApprovedAmountRequest.class)))
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(schema = @Schema(implementation = 
LoansApiResourceSwagger.PutLoansApprovedAmountResponse.class))) })
+    public CommandProcessingResult modifyLoanApprovedAmount(
+            @PathParam("loanExternalId") @Parameter(description = 
"loanExternalId", required = true) final String loanExternalId,
+            @Context final UriInfo uriInfo, @Parameter(hidden = true) final 
String apiRequestBodyAsJson) {
+        return modifyLoanApprovedAmount(null, 
ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
+    }
+
+    @GET
+    @Path("{loanId}/approved-amount")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Operation(summary = "Collects and returns the approved amount 
modification history for a given loan", description = "")
+    public List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(
+            @PathParam("loanId") @Parameter(description = "loanId", required = 
true) final Long loanId, @Context final UriInfo uriInfo) {
+        return getLoanApprovedAmountHistory(loanId, ExternalId.empty());
+    }
+
+    @GET
+    @Path("external-id/{loanExternalId}/approved-amount")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Operation(summary = "Collects and returns the approved amount 
modification history for a given loan", description = "")
+    public List<LoanApprovedAmountHistoryData> getLoanApprovedAmountHistory(
+            @PathParam("loanExternalId") @Parameter(description = 
"loanExternalId", required = true) final String loanExternalId,
+            @Context final UriInfo uriInfo) {
+        return getLoanApprovedAmountHistory(null, 
ExternalIdFactory.produce(loanExternalId));
+    }
+
     private String retrieveApprovalTemplate(final Long loanId, final String 
loanExternalIdStr, final String templateType,
             final UriInfo uriInfo) {
         
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
@@ -1294,4 +1348,19 @@ public class LoansApiResource {
         return delinquencyActionSerializer.serialize(result);
     }
 
+    private CommandProcessingResult modifyLoanApprovedAmount(Long loanId, 
ExternalId loanExternalId, String apiRequestBodyAsJson) {
+        Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
+        final CommandWrapperBuilder builder = new 
CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
+        CommandWrapper commandRequest = 
builder.updateLoanApprovedAmount(resolvedLoanId).build();
+
+        return 
this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
+    }
+
+    private List<LoanApprovedAmountHistoryData> 
getLoanApprovedAmountHistory(Long loanId, ExternalId loanExternalId) {
+        
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
+        Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
+        Pageable sortedByCreationDate = 
Pageable.unpaged(Sort.by("createdDate").ascending());
+        return 
loanApprovedAmountHistoryRepository.findAllByLoanId(resolvedLoanId, 
sortedByCreationDate);
+    }
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
index 3d4b1cdbc2..6c2252f1ea 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java
@@ -1771,4 +1771,47 @@ final class LoansApiResourceSwagger {
         @Schema(description = "PostLoansLoanIdChanges")
         public PostLoansLoanIdChanges changes;
     }
+
+    @Schema(description = "PutLoansApprovedAmountRequest")
+    public static final class PutLoansApprovedAmountRequest {
+
+        private PutLoansApprovedAmountRequest() {}
+
+        @Schema(example = "1000")
+        public BigDecimal amount;
+        @Schema(example = "en")
+        public String locale;
+    }
+
+    @Schema(description = "PutLoansApprovedAmountResponse")
+    public static final class PutLoansApprovedAmountResponse {
+
+        private PutLoansApprovedAmountResponse() {}
+
+        static final class PutLoansApprovedAmountChanges {
+
+            private PutLoansApprovedAmountChanges() {}
+
+            @Schema(example = "1000")
+            public BigDecimal oldApprovedAmount;
+            @Schema(example = "1000")
+            public BigDecimal newApprovedAmount;
+            @Schema(example = "en_GB")
+            public String locale;
+        }
+
+        @Schema(example = "3")
+        public Long resourceId;
+        @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
+        public String resourceExternalId;
+        @Schema(example = "2")
+        public Long officeId;
+        @Schema(example = "6")
+        public Long clientId;
+        @Schema(example = "10")
+        public Long groupId;
+
+        @Schema(description = "PutLoansApprovedAmountChanges")
+        public PutLoansApprovedAmountChanges changes;
+    }
 }
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 e75d377929..3c7fd4c9d5 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
@@ -2174,12 +2174,33 @@ public final class LoanApplicationValidator {
 
     public BigDecimal getOverAppliedMax(Loan loan) {
         LoanProduct loanProduct = loan.getLoanProduct();
+
+        // Check if overapplied calculation type and number are properly 
configured
+        if (loanProduct.getOverAppliedCalculationType() == null || 
loanProduct.getOverAppliedNumber() == null) {
+            // If overapplied calculation is not configured, return proposed 
principal (original behavior)
+            return loan.getProposedPrincipal();
+        }
+
+        // For loans with approved amount modifications, use proposed 
principal as base to allow
+        // disbursement up to the originally requested amount regardless of 
the reduced approved amount
+        boolean hasApprovedAmountModification = loan.getApprovedPrincipal() != 
null && loan.getProposedPrincipal() != null
+                && 
loan.getApprovedPrincipal().compareTo(loan.getProposedPrincipal()) != 0;
+
+        BigDecimal basePrincipal;
+        if (hasApprovedAmountModification) {
+            // Use proposed principal for loans with approved amount 
modifications
+            basePrincipal = loan.getProposedPrincipal();
+        } else {
+            // Use approved principal for normal loans
+            basePrincipal = loan.getApprovedPrincipal() != null ? 
loan.getApprovedPrincipal() : loan.getProposedPrincipal();
+        }
+
         if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) {
             BigDecimal overAppliedNumber = 
BigDecimal.valueOf(loanProduct.getOverAppliedNumber());
             BigDecimal totalPercentage = 
BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100)));
-            return loan.getProposedPrincipal().multiply(totalPercentage);
+            return basePrincipal.multiply(totalPercentage);
         } else {
-            return 
loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
+            return 
basePrincipal.add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
         }
     }
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java
new file mode 100644
index 0000000000..9ea5b444b3
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApprovedAmountValidatorImpl.java
@@ -0,0 +1,104 @@
+/**
+ * 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.portfolio.loanaccount.serialization;
+
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import org.apache.fineract.portfolio.common.service.Validator;
+import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import 
org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public final class LoanApprovedAmountValidatorImpl implements 
LoanApprovedAmountValidator {
+
+    private static final Set<LoanStatus> 
INVALID_LOAN_STATUSES_FOR_APPROVED_AMOUNT_MODIFICATION = 
Set.of(LoanStatus.INVALID,
+            LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.REJECTED);
+
+    private final FromJsonHelper fromApiJsonHelper;
+    private final LoanRepository loanRepository;
+    private final LoanApplicationValidator loanApplicationValidator;
+
+    @Override
+    public void validateLoanApprovedAmountModification(JsonCommand command) {
+        String json = command.json();
+        if (StringUtils.isBlank(json)) {
+            throw new InvalidJsonException();
+        }
+
+        final Set<String> supportedParameters = new HashSet<>(
+                Arrays.asList(LoanApiConstants.amountParameterName, 
LoanApiConstants.localeParameterName));
+
+        final JsonElement element = this.fromApiJsonHelper.parse(json);
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() 
{}.getType();
+        this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, 
supportedParameters);
+
+        final BigDecimal newApprovedAmount = 
this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.amountParameterName,
+                element);
+
+        Validator.validateOrThrow("loan.approved.amount", baseDataValidator -> 
{
+            
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newApprovedAmount).notNull();
+        });
+
+        Validator.validateOrThrowDomainViolation("loan.approved.amount", 
baseDataValidator -> {
+            
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName).value(newApprovedAmount).positiveAmount();
+
+            final Long loanId = command.getLoanId();
+            Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> 
new LoanNotFoundException(loanId));
+
+            if 
(INVALID_LOAN_STATUSES_FOR_APPROVED_AMOUNT_MODIFICATION.contains(loan.getStatus()))
 {
+                
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode("loan.status.not.valid.for.approved.amount.modification");
+            }
+
+            BigDecimal maximumThresholdForApprovedAmount;
+            if 
(loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
+                maximumThresholdForApprovedAmount = 
loanApplicationValidator.getOverAppliedMax(loan);
+            } else {
+                maximumThresholdForApprovedAmount = 
loan.getProposedPrincipal();
+            }
+
+            if (MathUtil.isGreaterThan(newApprovedAmount, 
maximumThresholdForApprovedAmount)) {
+                
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
+                        
.failWithCode("can't.be.greater.than.maximum.applied.loan.amount.calculation");
+            }
+
+            BigDecimal totalPrincipalOnLoan = 
loan.getSummary().getTotalPrincipal();
+            if (MathUtil.isLessThan(newApprovedAmount, totalPrincipalOnLoan)) {
+                
baseDataValidator.reset().parameter(LoanApiConstants.amountParameterName)
+                        
.failWithCode("less.than.disbursed.principal.and.capitalized.income");
+            }
+        });
+    }
+}
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 6b57292301..69be1ab519 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
@@ -44,7 +44,7 @@ public final class LoanDisbursementValidator {
             } 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 ";
+                    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 00716c1ee8..4847176e4f 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
@@ -158,7 +158,7 @@ public final class LoanTransactionValidatorImpl implements 
LoanTransactionValida
             validateLoanClientIsActive(loan);
             validateLoanGroupIsActive(loan);
 
-            final BigDecimal disbursedAmount = loan.getDisbursedAmount();
+            final BigDecimal disbursedAmount = 
loan.getSummary().getTotalPrincipalDisbursed();
             
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, 
principal, disbursedAmount);
 
             if (loan.isChargedOff()) {
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 428f0d4b2b..c7fe4e457e 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
@@ -161,6 +161,7 @@ public class LoanDisbursementService {
             } else {
                 loan.getLoanRepaymentScheduleDetail()
                         
.setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount());
+                totalAmount = 
loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
             }
             
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, 
disburseAmount.getAmount(), totalAmount);
         }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index 23731b0b61..70866c25c4 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -432,7 +432,7 @@ public class LoanAccountConfiguration {
             LoanJournalEntryPoster journalEntryPoster, LoanAdjustmentService 
loanAdjustmentService,
             LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanMapper 
loanMapper,
             LoanTransactionProcessingService loanTransactionProcessingService, 
final LoanBalanceService loanBalanceService,
-            LoanTransactionService loanTransactionService, 
BuyDownFeePlatformService buyDownFeePlatformService) {
+            LoanTransactionService loanTransactionService) {
         return new LoanWritePlatformServiceJpaRepositoryImpl(context, 
loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
                 loanRepositoryWrapper, loanAccountDomainService, 
noteRepository, loanTransactionRepository,
                 loanTransactionRelationRepository, loanAssembler, 
journalEntryWritePlatformService, calendarInstanceRepository,
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 5e627b4d8d..fa8027a4e8 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -209,4 +209,6 @@
     <include file="parts/0188_create_loan_buy_down_fee_balance.xml" 
relativeToChangelogFile="true" />
     <include file="parts/0189_add_loan_buydown_fee_event.xml" 
relativeToChangelogFile="true" />
     <include file="parts/0190_buy_down_fee_amortization.xml" 
relativeToChangelogFile="true" />
+    <include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" 
relativeToChangelogFile="true" />
+    <include file="parts/0192_create_loan_approved_amount_history.xml" 
relativeToChangelogFile="true" />
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml
new file mode 100644
index 0000000000..d58c2255b2
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+    <changeSet id="1" author="fineract">
+        <insert tableName="m_external_event_configuration">
+            <column name="type" 
value="LoanApprovedAmountChangedBusinessEvent"/>
+            <column name="enabled" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0192_create_loan_approved_amount_history.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0192_create_loan_approved_amount_history.xml
new file mode 100644
index 0000000000..1f0d1b284e
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0192_create_loan_approved_amount_history.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd";>
+    <changeSet author="fineract" id="1" context="postgresql">
+        <createTable tableName="m_loan_approved_amount_history">
+            <column autoIncrement="true" name="id" type="BIGINT">
+                <constraints nullable="false" primaryKey="true" 
primaryKeyName="pk_m_loan_approved_amount_history"/>
+            </column>
+            <column name="loan_id" type="BIGINT">
+                <constraints nullable="false"/>
+            </column>
+            <column name="new_approved_amount" type="DECIMAL(19, 6)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="old_approved_amount" type="DECIMAL(19, 6)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="created_by" type="BIGINT"/>
+            <column name="created_on_utc" type="timestamp with time zone"/>
+            <column name="last_modified_by" type="BIGINT"/>
+            <column name="last_modified_on_utc" type="timestamp with time 
zone"/>
+        </createTable>
+    </changeSet>
+    <changeSet author="fineract" id="2" context="mysql">
+        <createTable tableName="m_loan_approved_amount_history">
+            <column autoIncrement="true" name="id" type="BIGINT">
+                <constraints nullable="false" primaryKey="true" 
primaryKeyName="pk_m_loan_approved_amount_history"/>
+            </column>
+            <column name="loan_id" type="BIGINT">
+                <constraints nullable="false"/>
+            </column>
+            <column name="new_approved_amount" type="DECIMAL(19, 6)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="old_approved_amount" type="DECIMAL(19, 6)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="created_by" type="BIGINT"/>
+            <column name="created_on_utc" type="DATETIME"/>
+            <column name="last_modified_by" type="BIGINT"/>
+            <column name="last_modified_on_utc" type="DATETIME"/>
+        </createTable>
+    </changeSet>
+    <changeSet author="fineract" id="3">
+        <addForeignKeyConstraint baseColumnNames="created_by" 
baseTableName="m_loan_approved_amount_history"
+                                 
constraintName="FK_loan_approved_amount_history_created_by" deferrable="false" 
initiallyDeferred="false"
+                                 onDelete="RESTRICT" onUpdate="RESTRICT" 
referencedColumnNames="id"
+                                 referencedTableName="m_appuser" 
validate="true"/>
+        <addForeignKeyConstraint baseColumnNames="last_modified_by" 
baseTableName="m_loan_approved_amount_history"
+                                 
constraintName="FK_loan_approved_amount_history_last_modified_by" 
deferrable="false" initiallyDeferred="false"
+                                 onDelete="RESTRICT" onUpdate="RESTRICT" 
referencedColumnNames="id"
+                                 referencedTableName="m_appuser" 
validate="true"/>
+        <addForeignKeyConstraint baseColumnNames="loan_id" 
baseTableName="m_loan_approved_amount_history"
+                                 
constraintName="FK_loan_approved_amount_history_loan_id" deferrable="false" 
initiallyDeferred="false"
+                                 onDelete="RESTRICT" onUpdate="RESTRICT" 
referencedColumnNames="id"
+                                 referencedTableName="m_loan" validate="true"/>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index 3347b2162b..697534d2de 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -112,7 +112,7 @@ public class 
ExternalEventConfigurationValidationServiceTest {
                 "LoanCapitalizedIncomeTransactionCreatedBusinessEvent", 
"LoanUndoContractTerminationBusinessEvent",
                 "LoanBuyDownFeeTransactionCreatedBusinessEvent", 
"LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
                 "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
-                
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
+                
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", 
"LoanApprovedAmountChangedBusinessEvent");
 
         List<FineractPlatformTenant> tenants = Arrays
                 .asList(new FineractPlatformTenant(1L, "default", "Default 
Tenant", "Europe/Budapest", null));
@@ -206,7 +206,7 @@ public class 
ExternalEventConfigurationValidationServiceTest {
                 "LoanCapitalizedIncomeTransactionCreatedBusinessEvent", 
"LoanUndoContractTerminationBusinessEvent",
                 "LoanBuyDownFeeTransactionCreatedBusinessEvent", 
"LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
                 "LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
-                
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
+                
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", 
"LoanApprovedAmountChangedBusinessEvent");
 
         List<FineractPlatformTenant> tenants = Arrays
                 .asList(new FineractPlatformTenant(1L, "default", "Default 
Tenant", "Europe/Budapest", null));
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index dfe969ca41..788b5ea2ce 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -67,6 +67,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdStatus;
 import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
 import 
org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
 import org.apache.fineract.client.models.JournalEntryTransactionItem;
+import org.apache.fineract.client.models.LoanApprovedAmountHistoryData;
 import org.apache.fineract.client.models.LoanPointInTimeData;
 import org.apache.fineract.client.models.PaymentAllocationOrder;
 import org.apache.fineract.client.models.PostChargesResponse;
@@ -81,6 +82,8 @@ import 
org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionI
 import org.apache.fineract.client.models.PostLoansRequest;
 import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
+import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
+import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
 import org.apache.fineract.client.models.PutLoansLoanIdResponse;
 import org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
@@ -777,6 +780,15 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
         return 
Calls.ok(fineractClient().loansPointInTimeApi.retrieveLoansPointInTime(request));
     }
 
+    protected PutLoansApprovedAmountResponse modifyLoanApprovedAmount(Long 
loanId, BigDecimal approvedAmount) {
+        PutLoansApprovedAmountRequest request = new 
PutLoansApprovedAmountRequest().amount(approvedAmount).locale("en");
+        return 
Calls.ok(fineractClient().loans.modifyLoanApprovedAmount(loanId, request));
+    }
+
+    protected List<LoanApprovedAmountHistoryData> 
getLoanApprovedAmountHistory(Long loanId) {
+        return 
Calls.ok(fineractClient().loans.getLoanApprovedAmountHistory(loanId));
+    }
+
     protected void verifyOutstanding(LoanPointInTimeData loan, 
OutstandingAmounts outstanding) {
         assertThat(BigDecimal.valueOf(outstanding.principalOutstanding))
                 
.isEqualByComparingTo(loan.getPrincipal().getPrincipalOutstanding());
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTests.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTests.java
new file mode 100644
index 0000000000..93bb797089
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanModifyApprovedAmountTests.java
@@ -0,0 +1,481 @@
+/**
+ * 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.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.List;
+import java.util.Objects;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
+import org.apache.fineract.client.models.LoanApprovedAmountHistoryData;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansDisbursementData;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
+import org.apache.fineract.client.util.CallFailedRuntimeException;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class LoanModifyApprovedAmountTests extends BaseLoanIntegrationTest {
+
+    @Test
+    public void testValidLoanApprovedAmountModification() {
+        BigDecimal sixHundred = BigDecimal.valueOf(600.0);
+        BigDecimal thousand = BigDecimal.valueOf(1000.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+            PutLoansApprovedAmountResponse putLoansApprovedAmountResponse = 
modifyLoanApprovedAmount(loanId, sixHundred);
+
+            Assertions.assertEquals(loanId, 
putLoansApprovedAmountResponse.getResourceId());
+            
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges());
+            
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount());
+            
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount());
+            Assertions.assertEquals(sixHundred,
+                    
putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+            Assertions.assertEquals(thousand,
+                    
putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+        });
+    }
+
+    @Test
+    public void testLoanApprovedAmountModificationEvent() {
+        
externalEventHelper.enableBusinessEvent("LoanApprovedAmountChangedBusinessEvent");
+        BigDecimal sixHundred = BigDecimal.valueOf(600.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+
+            deleteAllExternalEvents();
+            modifyLoanApprovedAmount(loanId, sixHundred);
+
+            verifyBusinessEvents(new 
LoanBusinessEvent("LoanApprovedAmountChangedBusinessEvent", "01 January 2024", 
300, 100.0, 100.0));
+        });
+    }
+
+    @Test
+    public void testValidLoanApprovedAmountModificationInvalidRequest() {
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, null));
+
+            assertEquals(400, exception.getResponse().code());
+            
assertTrue(exception.getMessage().contains("validation.msg.loan.approved.amount.amount.cannot.be.blank"));
+        });
+    }
+
+    @Test
+    public void testValidLoanApprovedAmountModificationInvalidLoanStatus() {
+        BigDecimal sixHundred = BigDecimal.valueOf(600.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "1 January 2024", 
1000.0, 10.0, 4, null));
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> 
modifyLoanApprovedAmount(postLoansResponse.getResourceId(), sixHundred));
+
+            assertEquals(403, exception.getResponse().code());
+            assertTrue(exception.getMessage()
+                    
.contains("validation.msg.loan.approved.amount.loan.status.not.valid.for.approved.amount.modification"));
+        });
+    }
+
+    @Test
+    public void testModifyLoanApprovedAmountTooHigh() {
+        BigDecimal twoThousand = BigDecimal.valueOf(2000.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, twoThousand));
+
+            assertEquals(403, exception.getResponse().code());
+            assertTrue(exception.getMessage()
+                    
.contains("validation.msg.loan.approved.amount.amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
+        });
+    }
+
+    @Test
+    public void testModifyLoanApprovedAmountHigherButInRange() {
+        BigDecimal thousand = BigDecimal.valueOf(1000.0);
+        BigDecimal fifteenHundred = BigDecimal.valueOf(1500.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            PutLoansApprovedAmountResponse putLoansApprovedAmountResponse = 
modifyLoanApprovedAmount(loanId, fifteenHundred);
+
+            Assertions.assertEquals(loanId, 
putLoansApprovedAmountResponse.getResourceId());
+            
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges());
+            
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount());
+            
Assertions.assertNotNull(putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount());
+            Assertions.assertEquals(fifteenHundred,
+                    
putLoansApprovedAmountResponse.getChanges().getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+            Assertions.assertEquals(thousand,
+                    
putLoansApprovedAmountResponse.getChanges().getOldApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+        });
+    }
+
+    @Test
+    public void testModifyLoanApprovedAmountWithNegativeAmount() {
+        BigDecimal sixHundred = BigDecimal.valueOf(600.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, 
sixHundred.negate()));
+
+            assertEquals(403, exception.getResponse().code());
+            
assertTrue(exception.getMessage().contains("validation.msg.loan.approved.amount.amount.not.greater.than.zero"));
+        });
+    }
+
+    @Test
+    public void 
testModifyLoanApprovedAmountCapitalizedIncomeCountsAsPrincipal() {
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+                .createLoanProduct(create4IProgressiveWithCapitalizedIncome());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            disburseLoan(loanId, BigDecimal.valueOf(500), "1 January 2024");
+            PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = 
loanTransactionHelper.addCapitalizedIncome(loanId,
+                    "1 January 2024", 500.0);
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, 
BigDecimal.valueOf(500.0)));
+
+            assertEquals(403, exception.getResponse().code());
+            assertTrue(exception.getMessage()
+                    
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
+
+            
loanTransactionHelper.reverseLoanTransaction(capitalizedIncomeResponse.getLoanId(),
 capitalizedIncomeResponse.getResourceId(),
+                    "1 January 2024");
+
+            Assertions.assertDoesNotThrow(() -> 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(500.0)));
+        });
+    }
+
+    @Test
+    public void 
testModifyLoanApprovedAmountFutureExpectedDisbursementsCountAsPrincipal() {
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+                
.createLoanProduct(create4IProgressive().disallowExpectedDisbursements(false).allowApprovedDisbursedAmountsOverApplied(null)
+                        
.overAppliedCalculationType(null).overAppliedNumber(null));
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 7.0, 6, (request) -> 
request.disbursementData(List.of(new PostLoansDisbursementData()
+                            .expectedDisbursementDate("1 January 
2024").principal(BigDecimal.valueOf(1000.0)))));
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, 
BigDecimal.valueOf(500.0)));
+
+            assertEquals(403, exception.getResponse().code());
+            assertTrue(exception.getMessage()
+                    
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
+        });
+    }
+
+    @Test
+    public void testModifyLoanApprovedAmountCreatesHistoryEntries() {
+        BigDecimal fourHundred = BigDecimal.valueOf(400.0);
+        BigDecimal sixHundred = BigDecimal.valueOf(600.0);
+        BigDecimal eightHundred = BigDecimal.valueOf(800.0);
+        BigDecimal thousand = BigDecimal.valueOf(1000.0);
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0));
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(400.0));
+
+            List<LoanApprovedAmountHistoryData> loanApprovedAmountHistory = 
getLoanApprovedAmountHistory(loanId);
+
+            Assertions.assertNotNull(loanApprovedAmountHistory);
+            Assertions.assertEquals(3, loanApprovedAmountHistory.size());
+
+            Assertions.assertEquals(thousand, 
loanApprovedAmountHistory.get(0).getOldApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+            Assertions.assertEquals(eightHundred,
+                    
loanApprovedAmountHistory.get(0).getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+
+            Assertions.assertEquals(eightHundred,
+                    
loanApprovedAmountHistory.get(1).getOldApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+            Assertions.assertEquals(sixHundred, 
loanApprovedAmountHistory.get(1).getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+
+            Assertions.assertEquals(sixHundred, 
loanApprovedAmountHistory.get(2).getOldApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+            Assertions.assertEquals(fourHundred, 
loanApprovedAmountHistory.get(2).getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+        });
+    }
+
+    @Test
+    public void testDisbursementValidationAfterApprovedAmountReduction() {
+        // Test that disbursement validation properly respects reduced 
approved amounts
+        // Scenario: Reduce approved amount and verify disbursements are 
limited to new amount
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            // Create loan with applied amount $1000
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            // Reduce approved amount to $900
+            PutLoansApprovedAmountResponse modifyResponse = 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(900.0));
+            assertEquals(BigDecimal.valueOf(900.0), 
modifyResponse.getChanges().getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+
+            // Disburse $100 (should work as it's within approved amount)
+            Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, 
BigDecimal.valueOf(100), "1 January 2024"),
+                    "Should be able to disburse $100 after reducing approved 
amount to $900");
+
+            // Disburse additional $250 (total $350, should work as it's 
within proposed $1000 × 150% = $1350)
+            Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, 
BigDecimal.valueOf(250), "1 January 2024"),
+                    "Should be able to disburse additional $250 (total $350) 
within allowed limit");
+
+            // Try to disburse additional $1200 (total $1550, should fail as 
it exceeds $1000 × 150% = $1350)
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> disburseLoan(loanId, BigDecimal.valueOf(1200), "1 
January 2024"));
+            assertEquals(403, exception.getResponse().code());
+            
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"),
+                    "Should fail when total disbursements exceed modified 
approved amount × over-applied percentage");
+        });
+    }
+
+    @Test
+    public void testProgressiveDisbursementsWithDynamicApprovedAmountChanges() 
{
+        // Test multiple disbursements with increasing and decreasing approved 
amount modifications
+        // Validates that each disbursement respects the current approved 
amount limits
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            // Create loan with $1000 applied amount
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            // First disbursement: $300
+            disburseLoan(loanId, BigDecimal.valueOf(300), "1 January 2024");
+
+            // Increase approved amount to $1200
+            PutLoansApprovedAmountResponse increaseResponse = 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(1200.0));
+            assertEquals(BigDecimal.valueOf(1200.0),
+                    
increaseResponse.getChanges().getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+
+            // Second disbursement: $400 (total $700, within $1200)
+            Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, 
BigDecimal.valueOf(400), "1 January 2024"));
+
+            // Reduce approved amount to $800
+            PutLoansApprovedAmountResponse reduceResponse = 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
+            assertEquals(BigDecimal.valueOf(800.0), 
reduceResponse.getChanges().getNewApprovedAmount().setScale(1, 
RoundingMode.HALF_UP));
+
+            // Third disbursement: $100 (total $800, within proposed $1000 × 
150% = $1500)
+            Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, 
BigDecimal.valueOf(100), "1 January 2024"));
+
+            // Fourth disbursement: $800 (total $1600, should fail as it 
exceeds $1000 × 150% = $1500)
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> disburseLoan(loanId, BigDecimal.valueOf(800), "1 
January 2024"));
+            assertEquals(403, exception.getResponse().code());
+            
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
+        });
+    }
+
+    @Test
+    public void testApprovedAmountModificationWithCapitalizedIncomeScenario() {
+        // Test approved amount modification interaction with capitalized 
income
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+                .createLoanProduct(create4IProgressiveWithCapitalizedIncome());
+        runAt("1 January 2024", () -> {
+            // Create loan with $1000 applied amount
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            // Disburse $300
+            disburseLoan(loanId, BigDecimal.valueOf(300), "1 January 2024");
+
+            // Add capitalized income of $200 (total disbursed equivalent: 
$500)
+            loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 
2024", 200.0);
+
+            // Try to reduce approved amount to $400 (should fail as disbursed 
+ capitalized = $500)
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, 
BigDecimal.valueOf(400.0)));
+            assertEquals(403, exception.getResponse().code());
+            assertTrue(exception.getMessage()
+                    
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
+
+            // Should succeed with $500 (exactly matching disbursed + 
capitalized)
+            Assertions.assertDoesNotThrow(() -> 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(500.0)));
+
+            // Should succeed with $600 (above disbursed + capitalized)
+            Assertions.assertDoesNotThrow(() -> 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0)));
+        });
+    }
+
+    @Test
+    public void testUndoDisbursementAfterApprovedAmountReduction() {
+        // Test undo disbursement functionality after approved amount reduction
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
+            disburseLoan(loanId, BigDecimal.valueOf(600), "1 January 2024");
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            if (loanDetails.getSummary() != null && 
loanDetails.getSummary().getPrincipalDisbursed() != null) {
+                assertEquals(BigDecimal.valueOf(600.0), 
loanDetails.getSummary().getPrincipalDisbursed().setScale(1, 
RoundingMode.HALF_UP));
+            }
+
+            PostLoansLoanIdRequest undoRequest = new 
PostLoansLoanIdRequest().note("Undo disbursement for testing");
+            Assertions.assertDoesNotThrow(() -> 
loanTransactionHelper.undoDisbursalLoan(loanId, undoRequest));
+
+            GetLoansLoanIdResponse loanDetailsAfterUndo = 
loanTransactionHelper.getLoanDetails(loanId);
+            BigDecimal activeDisbursedAmount = BigDecimal.ZERO;
+            if (loanDetailsAfterUndo.getTransactions() != null && 
!loanDetailsAfterUndo.getTransactions().isEmpty()) {
+                activeDisbursedAmount = 
loanDetailsAfterUndo.getTransactions().stream()
+                        .filter(transaction -> transaction.getType() != null 
&& "Disbursement".equals(transaction.getType().getValue()))
+                        .filter(transaction -> 
!Boolean.TRUE.equals(transaction.getManuallyReversed()))
+                        
.map(GetLoansLoanIdTransactions::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO,
 BigDecimal::add);
+            }
+            assertEquals(0, BigDecimal.ZERO.compareTo(activeDisbursedAmount));
+
+            Assertions.assertDoesNotThrow(() -> 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(400.0)));
+
+            GetLoansLoanIdResponse finalLoanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertEquals(BigDecimal.valueOf(400.0), 
finalLoanDetails.getApprovedPrincipal().setScale(1, RoundingMode.HALF_UP));
+        });
+    }
+
+    @Test
+    public void testUndoLastDisbursementWithMultipleDisbursements() {
+        // Test undo last disbursement in multi-disbursement scenario with 
approved amount modifications
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            disburseLoan(loanId, BigDecimal.valueOf(300), "1 January 2024");
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(1200.0));
+            disburseLoan(loanId, BigDecimal.valueOf(400), "1 January 2024");
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(800.0));
+            disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            if (loanDetails.getSummary() != null && 
loanDetails.getSummary().getPrincipalDisbursed() != null) {
+                assertEquals(BigDecimal.valueOf(800.0), 
loanDetails.getSummary().getPrincipalDisbursed().setScale(1, 
RoundingMode.HALF_UP));
+            }
+
+            PostLoansLoanIdRequest undoLastRequest = new 
PostLoansLoanIdRequest().note("Undo last disbursement");
+            Assertions.assertDoesNotThrow(() -> 
loanTransactionHelper.undoLastDisbursalLoan(loanId, undoLastRequest));
+
+            GetLoansLoanIdResponse loanDetailsAfterUndo = 
loanTransactionHelper.getLoanDetails(loanId);
+            BigDecimal activeDisbursedAmount = BigDecimal.ZERO;
+            if (loanDetailsAfterUndo.getTransactions() != null && 
!loanDetailsAfterUndo.getTransactions().isEmpty()) {
+                activeDisbursedAmount = 
loanDetailsAfterUndo.getTransactions().stream()
+                        .filter(transaction -> transaction.getType() != null 
&& "Disbursement".equals(transaction.getType().getValue()))
+                        .filter(transaction -> 
!Boolean.TRUE.equals(transaction.getManuallyReversed()))
+                        
.map(GetLoansLoanIdTransactions::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO,
 BigDecimal::add);
+            }
+            assertEquals(BigDecimal.valueOf(700.0), 
activeDisbursedAmount.setScale(1, RoundingMode.HALF_UP));
+
+            Assertions.assertDoesNotThrow(() -> 
modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(700.0)));
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> modifyLoanApprovedAmount(loanId, 
BigDecimal.valueOf(600.0)));
+            assertEquals(403, exception.getResponse().code());
+            assertTrue(exception.getMessage()
+                    
.contains("validation.msg.loan.approved.amount.amount.less.than.disbursed.principal.and.capitalized.income"));
+        });
+    }
+
+    @Test
+    public void testDisbursementValidationAfterUndoWithReducedApprovedAmount() 
{
+        // Test disbursement validation after undo disbursement with reduced 
approved amount
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+        final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive());
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    1000.0, 10.0, 4, null);
+
+            modifyLoanApprovedAmount(loanId, BigDecimal.valueOf(600.0));
+            disburseLoan(loanId, BigDecimal.valueOf(500), "1 January 2024");
+
+            PostLoansLoanIdRequest undoRequest = new 
PostLoansLoanIdRequest().note("Undo for testing validation");
+            loanTransactionHelper.undoDisbursalLoan(loanId, undoRequest);
+
+            Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, 
BigDecimal.valueOf(700), "1 January 2024"));
+
+            loanTransactionHelper.undoDisbursalLoan(loanId, undoRequest);
+
+            CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> disburseLoan(loanId, BigDecimal.valueOf(1600), "1 
January 2024"));
+            assertEquals(403, exception.getResponse().code());
+            
assertTrue(exception.getMessage().contains("amount.can't.be.greater.than.maximum.applied.loan.amount.calculation"));
+        });
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 5970b58265..86651e3376 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -657,6 +657,11 @@ public class ExternalEventConfigurationHelper {
         
loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent.put("enabled",
 false);
         
defaults.add(loanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent);
 
+        Map<String, Object> loanApprovedAmountChangedBusinessEvent = new 
HashMap<>();
+        loanApprovedAmountChangedBusinessEvent.put("type", 
"LoanApprovedAmountChangedBusinessEvent");
+        loanApprovedAmountChangedBusinessEvent.put("enabled", false);
+        defaults.add(loanApprovedAmountChangedBusinessEvent);
+
         return defaults;
     }
 
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTestLifecycleExtension.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTestLifecycleExtension.java
index 8a6c1d382f..d10508569b 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTestLifecycleExtension.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTestLifecycleExtension.java
@@ -29,7 +29,9 @@ import 
org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import 
org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
 import org.apache.fineract.client.models.PostLoansLoanIdRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
 import org.apache.fineract.client.util.Calls;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
 import org.apache.fineract.integrationtests.common.BusinessDateHelper;
 import org.apache.fineract.integrationtests.common.FineractClientHelper;
 import org.apache.fineract.integrationtests.common.Utils;
@@ -52,6 +54,13 @@ public class LoanTestLifecycleExtension implements 
AfterEachCallback {
             loanIds.forEach(loanId -> {
                 GetLoansLoanIdResponse loanResponse = Calls
                         
.ok(FineractClientHelper.getFineractClient().loans.retrieveLoan((long) loanId, 
null, "all", null, null));
+                if (MathUtil.isLessThan(loanResponse.getApprovedPrincipal(), 
loanResponse.getProposedPrincipal())) {
+                    // reset approved principal in case it's less than 
proposed principal so all expected disbursements
+                    // can be properly disbursed
+                    PutLoansApprovedAmountRequest request = new 
PutLoansApprovedAmountRequest().amount(loanResponse.getProposedPrincipal())
+                            .locale("en");
+                    
Calls.ok(FineractClientHelper.getFineractClient().loans.modifyLoanApprovedAmount(loanId,
 request));
+                }
                 
loanResponse.getDisbursementDetails().forEach(disbursementDetail -> {
                     if (disbursementDetail.getActualDisbursementDate() == 
null) {
                         loanTransactionHelper.disburseLoan((long) loanId,

Reply via email to