You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2024/10/12 06:05:35 UTC

(fineract) branch develop updated: FINERACT-2081: Add all loan term validations to loan details

This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 25866edfc FINERACT-2081: Add all loan term validations to loan details
25866edfc is described below

commit 25866edfc93827b65ca3137a6ce32a5b772fe0c1
Author: Oleksii Novikov <al...@onix-systems.com>
AuthorDate: Thu Oct 3 14:21:01 2024 +0300

    FINERACT-2081: Add all loan term validations to loan details
---
 .../src/main/avro/loan/v1/LoanAccountDataV1.avsc   |  11 +
 .../dataqueries/api/DataTableApiConstant.java      |   1 +
 .../fineract/test/helper/ErrorMessageHelper.java   |  15 ++
 .../fineract/test/stepdef/loan/LoanStepDef.java    | 294 +++++++++++++++------
 .../src/test/resources/features/Loan.feature       |  29 ++
 .../loanaccount/data/LoanTermVariationsData.java   |  11 +
 .../portfolio/loanaccount/domain/Loan.java         |  14 +-
 .../domain/LoanTermVariationsRepository.java       |  22 +-
 .../loan/LoanBusinessEventSerializer.java          |   8 +-
 .../jobs/domain/JobExecutionRepository.java        |   2 +-
 .../loanaccount/api/LoansApiResource.java          |  18 +-
 .../loanaccount/api/LoansApiResourceSwagger.java   |  48 +++-
 .../loanaccount/data/LoanAccountData.java          |   5 +-
 ...nRescheduleRequestWritePlatformServiceImpl.java |   4 +-
 .../service/LoanReadPlatformService.java           |   3 -
 .../service/LoanReadPlatformServiceImpl.java       |  29 --
 .../ClientLoanIntegrationTest.java                 |   3 +
 .../ExternalBusinessEventTest.java                 |  27 +-
 18 files changed, 383 insertions(+), 161 deletions(-)

diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
index c56a9fc76..b97a2153c 100644
--- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
+++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
@@ -607,6 +607,17 @@
                 }
             ]
         },
+        {
+            "default": null,
+            "name": "loanTermVariations",
+            "type": [
+                "null",
+                {
+                    "type": "array",
+                    "items": "org.apache.fineract.avro.loan.v1.LoanTermVariationsDataV1"
+                }
+            ]
+        },
         {
             "default": null,
             "name": "clientActiveLoanOptions",
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java
index 85bb7fb8e..5cddec376 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/dataqueries/api/DataTableApiConstant.java
@@ -49,6 +49,7 @@ public final class DataTableApiConstant {
     public static final String linkedAccountAssociateParamName = "linkedAccount";
     public static final String multiDisburseDetailsAssociateParamName = "multiDisburseDetails";
     public static final String futureScheduleAssociateParamName = "futureSchedule";
+    public static final String loanTermVariationsAssociateParamName = "loanTermVariations";
     public static final String meetingAssociateParamName = "meeting";
     public static final String emiAmountVariationsAssociateParamName = "emiAmountVariations";
     public static final String collectionAssociateParamName = "collection";
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 357db71a4..469c96d9b 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
@@ -24,6 +24,7 @@ import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.fineract.client.models.BatchResponse;
 import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
@@ -864,4 +865,18 @@ public final class ErrorMessageHelper {
         return String.format("Wrong value in LoanDeteils/fixedLength. %nActual value is: %s %nExpected Value is: %s", actualToStr,
                 expectedToStr);
     }
+
+    public static String wrongValueInLineInLoanTermVariations(final int line, final List<List<String>> actual,
+            final List<String> expected) {
+        final String actualValues = actual.stream().map(List::toString).collect(Collectors.joining(System.lineSeparator()));
+
+        return String.format(
+                "%nWrong value in loan term variations tab line %d.%nActual values in line (with the same term variation applicable from) are:%n%s%nExpected values in line:%n%s",
+                line, actualValues, expected);
+    }
+
+    public static String wrongNumberOfLinesInLoanTermVariations(final int actual, final int expected) {
+        return String.format("Number of lines in loan term variations is not correct. Actual value is: %d - Expected value is: %d", actual,
+                expected);
+    }
 }
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 9b36c3828..4fdf238d2 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
@@ -21,6 +21,7 @@ package org.apache.fineract.test.stepdef.loan;
 import static org.apache.fineract.test.data.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import com.google.gson.Gson;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -54,6 +55,7 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
 import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData;
 import org.apache.fineract.client.models.GetLoansLoanIdLoanChargePaidByData;
+import org.apache.fineract.client.models.GetLoansLoanIdLoanTermVariations;
 import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
 import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
@@ -369,74 +371,15 @@ public class LoanStepDef extends AbstractStepDef {
     }
 
     @When("Admin creates a fully customized loan with the following data:")
-    public void createFullyCustomizedLoan(DataTable table) throws IOException {
-        List<List<String>> data = table.asLists();
-        List<String> loanData = data.get(1);
-        String loanProduct = loanData.get(0);
-        String submitDate = loanData.get(1);
-        String principal = loanData.get(2);
-        BigDecimal interestRate = new BigDecimal(loanData.get(3));
-        String interestType = loanData.get(4);
-        String interestCalculationPeriod = loanData.get(5);
-        String amortizationType = loanData.get(6);
-        Integer loanTermFrequency = Integer.valueOf(loanData.get(7));
-        String loanTermFrequencyType = loanData.get(8);
-        Integer repaymentFrequency = Integer.valueOf(loanData.get(9));
-        String repaymentFrequencyType = loanData.get(10);
-        Integer numberOfRepayments = Integer.valueOf(loanData.get(11));
-        Integer graceOnPrincipalPayment = Integer.valueOf(loanData.get(12));
-        Integer graceOnInterestPayment = Integer.valueOf(loanData.get(13));
-        Integer graceOnInterestCharged = Integer.valueOf(loanData.get(14));
-        String transactionProcessingStrategyCode = loanData.get(15);
-
-        Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
-        Long clientId = clientResponse.body().getClientId();
-
-        DefaultLoanProduct product = DefaultLoanProduct.valueOf(loanProduct);
-        Long loanProductId = loanProductResolver.resolve(product);
-
-        LoanTermFrequencyType termFrequencyType = LoanTermFrequencyType.valueOf(loanTermFrequencyType);
-        Integer loanTermFrequencyTypeValue = termFrequencyType.getValue();
-
-        RepaymentFrequencyType repaymentFrequencyType1 = RepaymentFrequencyType.valueOf(repaymentFrequencyType);
-        Integer repaymentFrequencyTypeValue = repaymentFrequencyType1.getValue();
-
-        InterestType interestType1 = InterestType.valueOf(interestType);
-        Integer interestTypeValue = interestType1.getValue();
-
-        InterestCalculationPeriodTime interestCalculationPeriod1 = InterestCalculationPeriodTime.valueOf(interestCalculationPeriod);
-        Integer interestCalculationPeriodValue = interestCalculationPeriod1.getValue();
-
-        AmortizationType amortizationType1 = AmortizationType.valueOf(amortizationType);
-        Integer amortizationTypeValue = amortizationType1.getValue();
-
-        TransactionProcessingStrategyCode processingStrategyCode = TransactionProcessingStrategyCode
-                .valueOf(transactionProcessingStrategyCode);
-        String transactionProcessingStrategyCodeValue = processingStrategyCode.getValue();
-
-        PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId)//
-                .productId(loanProductId)//
-                .principal(new BigDecimal(principal))//
-                .interestRatePerPeriod(interestRate)//
-                .interestType(interestTypeValue)//
-                .interestCalculationPeriodType(interestCalculationPeriodValue)//
-                .amortizationType(amortizationTypeValue)//
-                .loanTermFrequency(loanTermFrequency)//
-                .loanTermFrequencyType(loanTermFrequencyTypeValue)//
-                .numberOfRepayments(numberOfRepayments)//
-                .repaymentEvery(repaymentFrequency)//
-                .repaymentFrequencyType(repaymentFrequencyTypeValue)//
-                .submittedOnDate(submitDate)//
-                .expectedDisbursementDate(submitDate)//
-                .graceOnPrincipalPayment(graceOnPrincipalPayment)//
-                .graceOnInterestPayment(graceOnInterestPayment)//
-                .graceOnInterestPayment(graceOnInterestCharged).transactionProcessingStrategyCode(transactionProcessingStrategyCodeValue);//
-
-        Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
-        testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
-        ErrorHelper.checkSuccessfulApiCall(response);
+    public void createFullyCustomizedLoan(final DataTable table) throws IOException {
+        final List<List<String>> data = table.asLists();
+        createCustomizedLoan(data.get(1), false);
+    }
 
-        eventCheckHelper.createLoanEventCheck(response);
+    @When("Admin creates a fully customized loan with emi and the following data:")
+    public void createFullyCustomizedLoanWithEmi(final DataTable table) throws IOException {
+        final List<List<String>> data = table.asLists();
+        createCustomizedLoan(data.get(1), true);
     }
 
     @When("Admin creates a fully customized loan with fixed length {int} and with the following data:")
@@ -842,24 +785,36 @@ public class LoanStepDef extends AbstractStepDef {
 
     @And("Admin successfully disburse the loan on {string} with {string} EUR transaction amount")
     public void disburseLoan(String actualDisbursementDate, String transactionAmount) throws IOException {
-        Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
-        long loanId = loanResponse.body().getLoanId();
-        PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
+        final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        assertNotNull(loanResponse.body());
+        final long loanId = loanResponse.body().getLoanId();
+        final PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
                 .actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount));
+        performLoanDisbursementAndVerifyStatus(loanId, disburseRequest);
+    }
 
-        Response<PostLoansLoanIdResponse> loanDisburseResponse = loansApi.stateTransitions(loanId, disburseRequest, "disburse").execute();
-        testContext().set(TestContextKey.LOAN_DISBURSE_RESPONSE, loanDisburseResponse);
-        ErrorHelper.checkSuccessfulApiCall(loanDisburseResponse);
-        Long statusActual = loanDisburseResponse.body().getChanges().getStatus().getId();
-
-        Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "", "", "").execute();
-        Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId());
+    @And("Admin successfully disburse the loan on {string} with {string} EUR transaction amount and {string} fixed emi amount")
+    public void disburseLoanWithFixedEmiAmount(final String actualDisbursementDate, final String transactionAmount,
+            final String fixedEmiAmount) throws IOException {
+        final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        assertNotNull(loanResponse.body());
+        final long loanId = loanResponse.body().getLoanId();
+        final PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
+                .actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount))
+                .fixedEmiAmount(new BigDecimal(fixedEmiAmount));
+        performLoanDisbursementAndVerifyStatus(loanId, disburseRequest);
+    }
 
-        assertThat(statusActual)//
-                .as(ErrorMessageHelper.wrongLoanStatus(Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))//
-                .isEqualTo(statusExpected);//
-        eventCheckHelper.disburseLoanEventCheck(loanDisburseResponse);
-        eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse);
+    @And("Admin successfully disburse the loan on {string} with {string} EUR transaction amount, {string} EUR fixed emi amount and adjust repayment date on {string}")
+    public void disburseLoanWithFixedEmiAmountAndAdjustRepaymentDate(final String actualDisbursementDate, final String transactionAmount,
+            final String fixedEmiAmount, final String adjustRepaymentDate) throws IOException {
+        final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        assertNotNull(loanResponse.body());
+        final long loanId = loanResponse.body().getLoanId();
+        final PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
+                .actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount))
+                .fixedEmiAmount(new BigDecimal(fixedEmiAmount)).adjustRepaymentDate(adjustRepaymentDate);
+        performLoanDisbursementAndVerifyStatus(loanId, disburseRequest);
     }
 
     @And("Admin successfully disburse the second loan on {string} with {string} EUR transaction amount")
@@ -1939,6 +1894,155 @@ public class LoanStepDef extends AbstractStepDef {
                 .isEqualTo(ErrorMessageHelper.disbursePastDateFailure((int) loanId, futureApproveDateISO));
     }
 
+    @Then("Loan emi amount variations has {int} variation, with the following data:")
+    public void loanEmiAmountVariationsCheck(final int linesExpected, final DataTable table) throws IOException {
+        final Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        assertNotNull(loanCreateResponse.body());
+        final long loanId = loanCreateResponse.body().getLoanId();
+
+        final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "all", "", "").execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        final List<GetLoansLoanIdLoanTermVariations> emiAmountVariations = loanDetailsResponse.body().getEmiAmountVariations();
+
+        final List<List<String>> data = table.asLists();
+        assertNotNull(emiAmountVariations);
+        final int linesActual = emiAmountVariations.size();
+        data.stream().skip(1) // skip headers
+                .forEach(expectedValues -> {
+                    final List<List<String>> actualValuesList = emiAmountVariations.stream()
+                            .map(emi -> fetchValuesOfLoanTermVariations(data.get(0), emi)).collect(Collectors.toList());
+
+                    final boolean containsExpectedValues = actualValuesList.stream()
+                            .anyMatch(actualValues -> actualValues.equals(expectedValues));
+                    assertThat(containsExpectedValues).as(ErrorMessageHelper
+                            .wrongValueInLineInLoanTermVariations(data.indexOf(expectedValues), actualValuesList, expectedValues)).isTrue();
+
+                    assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInLoanTermVariations(linesActual, linesExpected))
+                            .isEqualTo(linesExpected);
+                });
+    }
+
+    @Then("Loan term variations has {int} variation, with the following data:")
+    public void loanTermVariationsCheck(final int linesExpected, final DataTable table) throws IOException {
+        final Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+        assertNotNull(loanCreateResponse.body());
+        final long loanId = loanCreateResponse.body().getLoanId();
+
+        final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "loanTermVariations", "", "")
+                .execute();
+        ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+        final List<GetLoansLoanIdLoanTermVariations> loanTermVariations = loanDetailsResponse.body().getLoanTermVariations();
+        assertNotNull(loanTermVariations);
+
+        final List<List<String>> data = table.asLists();
+        final int linesActual = loanTermVariations.size();
+        data.stream().skip(1) // skip headers
+                .forEach(expectedValues -> {
+                    final String expectedTermTypeId = expectedValues.get(0);
+
+                    final List<List<String>> actualValuesList = loanTermVariations.stream().filter(loanTerm -> {
+                        assertNotNull(loanTerm.getTermType());
+                        return expectedTermTypeId.equals(String.valueOf(loanTerm.getTermType().getId()));
+                    }).map(loanTerm -> fetchValuesOfLoanTermVariations(data.get(0), loanTerm)).collect(Collectors.toList());
+
+                    final boolean containsExpectedValues = actualValuesList.stream()
+                            .anyMatch(actualValues -> actualValues.equals(expectedValues));
+                    assertThat(containsExpectedValues).as(ErrorMessageHelper
+                            .wrongValueInLineInLoanTermVariations(data.indexOf(expectedValues), actualValuesList, expectedValues)).isTrue();
+
+                    assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInLoanTermVariations(linesActual, linesExpected))
+                            .isEqualTo(linesExpected);
+                });
+    }
+
+    private void createCustomizedLoan(final List<String> loanData, final boolean withEmi) throws IOException {
+        final String loanProduct = loanData.get(0);
+        final String submitDate = loanData.get(1);
+        final String principal = loanData.get(2);
+        final BigDecimal interestRate = new BigDecimal(loanData.get(3));
+        final String interestType = loanData.get(4);
+        final String interestCalculationPeriod = loanData.get(5);
+        final String amortizationType = loanData.get(6);
+        final Integer loanTermFrequency = Integer.valueOf(loanData.get(7));
+        final String loanTermFrequencyType = loanData.get(8);
+        final Integer repaymentFrequency = Integer.valueOf(loanData.get(9));
+        final String repaymentFrequencyType = loanData.get(10);
+        final Integer numberOfRepayments = Integer.valueOf(loanData.get(11));
+        final Integer graceOnPrincipalPayment = Integer.valueOf(loanData.get(12));
+        final Integer graceOnInterestPayment = Integer.valueOf(loanData.get(13));
+        final Integer graceOnInterestCharged = Integer.valueOf(loanData.get(14));
+        final String transactionProcessingStrategyCode = loanData.get(15);
+
+        final Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
+        final Long clientId = clientResponse.body().getClientId();
+
+        final DefaultLoanProduct product = DefaultLoanProduct.valueOf(loanProduct);
+        final Long loanProductId = loanProductResolver.resolve(product);
+
+        final LoanTermFrequencyType termFrequencyType = LoanTermFrequencyType.valueOf(loanTermFrequencyType);
+        final Integer loanTermFrequencyTypeValue = termFrequencyType.getValue();
+
+        final RepaymentFrequencyType repaymentFrequencyType1 = RepaymentFrequencyType.valueOf(repaymentFrequencyType);
+        final Integer repaymentFrequencyTypeValue = repaymentFrequencyType1.getValue();
+
+        final InterestType interestType1 = InterestType.valueOf(interestType);
+        final Integer interestTypeValue = interestType1.getValue();
+
+        final InterestCalculationPeriodTime interestCalculationPeriod1 = InterestCalculationPeriodTime.valueOf(interestCalculationPeriod);
+        final Integer interestCalculationPeriodValue = interestCalculationPeriod1.getValue();
+
+        final AmortizationType amortizationType1 = AmortizationType.valueOf(amortizationType);
+        final Integer amortizationTypeValue = amortizationType1.getValue();
+
+        final TransactionProcessingStrategyCode processingStrategyCode = TransactionProcessingStrategyCode
+                .valueOf(transactionProcessingStrategyCode);
+        final String transactionProcessingStrategyCodeValue = processingStrategyCode.getValue();
+
+        final PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId).productId(loanProductId)
+                .principal(new BigDecimal(principal)).interestRatePerPeriod(interestRate).interestType(interestTypeValue)
+                .interestCalculationPeriodType(interestCalculationPeriodValue).amortizationType(amortizationTypeValue)
+                .loanTermFrequency(loanTermFrequency).loanTermFrequencyType(loanTermFrequencyTypeValue)
+                .numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentFrequency)
+                .repaymentFrequencyType(repaymentFrequencyTypeValue).submittedOnDate(submitDate).expectedDisbursementDate(submitDate)
+                .graceOnPrincipalPayment(graceOnPrincipalPayment).graceOnInterestPayment(graceOnInterestPayment)
+                .graceOnInterestPayment(graceOnInterestCharged).transactionProcessingStrategyCode(transactionProcessingStrategyCodeValue);
+
+        if (withEmi) {
+            loansRequest.fixedEmiAmount(new BigDecimal(555));
+        }
+
+        final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
+        testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
+        ErrorHelper.checkSuccessfulApiCall(response);
+
+        eventCheckHelper.createLoanEventCheck(response);
+    }
+
+    private void performLoanDisbursementAndVerifyStatus(final long loanId, final PostLoansLoanIdRequest disburseRequest)
+            throws IOException {
+        final Response<PostLoansLoanIdResponse> loanDisburseResponse = loansApi.stateTransitions(loanId, disburseRequest, "disburse")
+                .execute();
+        testContext().set(TestContextKey.LOAN_DISBURSE_RESPONSE, loanDisburseResponse);
+        ErrorHelper.checkSuccessfulApiCall(loanDisburseResponse);
+        assertNotNull(loanDisburseResponse.body());
+        assertNotNull(loanDisburseResponse.body().getChanges());
+        assertNotNull(loanDisburseResponse.body().getChanges().getStatus());
+        final Long statusActual = loanDisburseResponse.body().getChanges().getStatus().getId();
+        assertNotNull(statusActual);
+
+        final Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "", "", "").execute();
+        assertNotNull(loanDetails.body());
+        assertNotNull(loanDetails.body().getStatus());
+        final Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId());
+
+        assertThat(statusActual).as(ErrorMessageHelper.wrongLoanStatus(Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))
+                .isEqualTo(statusExpected);
+        eventCheckHelper.disburseLoanEventCheck(loanDisburseResponse);
+        eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse);
+    }
+
     private LoanStatusEnumDataV1 getExpectedStatus(String loanStatus) {
         LoanStatusEnumDataV1 result = new LoanStatusEnumDataV1();
         switch (loanStatus) {
@@ -2137,4 +2241,30 @@ public class LoanStepDef extends AbstractStepDef {
         }
         return actualValues;
     }
+
+    @SuppressFBWarnings("SF_SWITCH_NO_DEFAULT")
+    private List<String> fetchValuesOfLoanTermVariations(final List<String> header, final GetLoansLoanIdLoanTermVariations emiVariation) {
+        final List<String> actualValues = new ArrayList<>();
+        assertNotNull(emiVariation.getTermType());
+        for (String headerName : header) {
+            switch (headerName) {
+                case "Term Type Id" -> actualValues
+                        .add(emiVariation.getTermType().getId() == null ? null : String.valueOf(emiVariation.getTermType().getId()));
+                case "Term Type Code" ->
+                    actualValues.add(emiVariation.getTermType().getCode() == null ? null : emiVariation.getTermType().getCode());
+                case "Term Type Value" ->
+                    actualValues.add(emiVariation.getTermType().getValue() == null ? null : emiVariation.getTermType().getValue());
+                case "Applicable From" -> actualValues.add(emiVariation.getTermVariationApplicableFrom() == null ? null
+                        : FORMATTER.format(emiVariation.getTermVariationApplicableFrom()));
+                case "Decimal Value" ->
+                    actualValues.add(emiVariation.getDecimalValue() == null ? null : String.valueOf(emiVariation.getDecimalValue()));
+                case "Date Value" ->
+                    actualValues.add(emiVariation.getDateValue() == null ? null : FORMATTER.format(emiVariation.getDateValue()));
+                case "Is Specific To Installment" -> actualValues.add(String.valueOf(emiVariation.getIsSpecificToInstallment()));
+                case "Is Processed" ->
+                    actualValues.add(emiVariation.getIsProcessed() == null ? null : String.valueOf(emiVariation.getIsProcessed()));
+            }
+        }
+        return actualValues;
+    }
 }
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
index 3af5410f0..938813a1c 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature
@@ -5728,3 +5728,32 @@ Feature: Loan
       | 5  | 31   | 01 June 2024     |                  | 16.93           | 16.81         | 0.2      | 0.0  | 0.0       | 17.01 | 0.0   | 0.0        | 0.0  | 17.01       |
       | 6  | 30   | 01 July 2024     |                  | 0.0             | 16.93         | 0.1      | 0.0  | 0.0       | 17.03 | 0.0   | 0.0        | 0.0  | 17.03       |
     When Admin removes "LOAN_INTEREST_RECALCULATION" business step into LOAN_CLOSE_OF_BUSINESS workflow
+
+  Scenario: Loan Details Emi Amount Variations - AssociationsAll
+    Given Global configuration "is-interest-to-be-recovered-first-when-greater-than-emi" is enabled
+    Given Global configuration "enable-business-date" is enabled
+    When Admin sets the business date to "1 January 2023"
+    When Admin creates a client with random data
+    When Admin creates a fully customized loan with emi and the following data:
+      | LoanProduct                                                                                     | submitted on date | with Principal | ANNUAL interest rate % | interest type     | interest calculation period | amortization type  | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy                        |
+      | LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB   | 01 January 2023   | 10000          | 12                     | DECLINING_BALANCE | SAME_AS_REPAYMENT_PERIOD    | EQUAL_INSTALLMENTS | 6                 | MONTHS                | 1              | MONTHS                 | 6                  | 0                       | 0                      | 0                    | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
+    And Admin successfully approves the loan on "1 January 2023" with "100" amount and expected disbursement date on "1 January 2023"
+    When Admin successfully disburse the loan on "1 January 2023" with "100" EUR transaction amount and "50" fixed emi amount
+    Then Loan emi amount variations has 1 variation, with the following data:
+      | Term Type Id | Term Type Code         | Term Type Value | Applicable From | Decimal Value | Date Value      | Is Specific To Installment | Is Processed |
+      | 1            | loanTermType.emiAmount | emiAmount       | 01 January 2023 | 50.0          |                 | false                      |              |
+
+  Scenario: Loan Details Loan Term Variations
+    Given Global configuration "is-interest-to-be-recovered-first-when-greater-than-emi" is enabled
+    Given Global configuration "enable-business-date" is enabled
+    When Admin sets the business date to "1 January 2023"
+    When Admin creates a client with random data
+    When Admin creates a fully customized loan with emi and the following data:
+      | LoanProduct                                                                                     | submitted on date | with Principal | ANNUAL interest rate % | interest type     | interest calculation period | amortization type  | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy                        |
+      | LP1_INTEREST_DECLINING_BALANCE_SAR_RECALCULATION_SAME_AS_REPAYMENT_COMPOUNDING_NONE_MULTIDISB   | 01 January 2023   | 10000          | 12                     | DECLINING_BALANCE | SAME_AS_REPAYMENT_PERIOD    | EQUAL_INSTALLMENTS | 6                 | MONTHS                | 1              | MONTHS                 | 6                  | 0                       | 0                      | 0                    | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER |
+    And Admin successfully approves the loan on "1 January 2023" with "100" amount and expected disbursement date on "1 January 2023"
+    When Admin successfully disburse the loan on "1 January 2023" with "100" EUR transaction amount, "50" EUR fixed emi amount and adjust repayment date on "15 January 2023"
+    Then Loan term variations has 2 variation, with the following data:
+      | Term Type Id | Term Type Code         | Term Type Value | Applicable From  | Decimal Value | Date Value      | Is Specific To Installment | Is Processed |
+      | 1            | loanTermType.emiAmount | emiAmount       | 01 January 2023  | 50.0          |                 | false                      |              |
+      | 4            | loanTermType.dueDate   | dueDate         | 01 February 2023 | 50.0          | 15 January 2023 | false                      |              |
\ No newline at end of file
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java
index 7b9858f07..55ee574bf 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsData.java
@@ -24,6 +24,7 @@ import lombok.Getter;
 import org.apache.fineract.infrastructure.core.data.EnumOptionData;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
+import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
 
 @Getter
 public class LoanTermVariationsData implements Comparable<LoanTermVariationsData> {
@@ -46,6 +47,16 @@ public class LoanTermVariationsData implements Comparable<LoanTermVariationsData
         this.isSpecificToInstallment = isSpecificToInstallment;
     }
 
+    public LoanTermVariationsData(final Long id, final Integer termType, final LocalDate termVariationApplicableFrom,
+            final BigDecimal decimalValue, final LocalDate dateValue, final boolean isSpecificToInstallment) {
+        this.id = id;
+        this.termType = LoanEnumerations.loanVariationType(LoanTermVariationType.fromInt(termType));
+        this.termVariationApplicableFrom = termVariationApplicableFrom;
+        this.decimalValue = decimalValue;
+        this.dateValue = dateValue;
+        this.isSpecificToInstallment = isSpecificToInstallment;
+    }
+
     public LoanTermVariationsData(final EnumOptionData termType, final LocalDate termVariationApplicableFrom, final BigDecimal decimalValue,
             LocalDate dateValue, final boolean isSpecificToInstallment) {
         this.id = null;
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 1ac524f9b..82f58a308 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -5366,16 +5366,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
         return isForeClosure;
     }
 
-    public Set<LoanTermVariations> getActiveLoanTermVariations() {
-        Set<LoanTermVariations> retData = new HashSet<>();
-        if (this.loanTermVariations != null && !this.loanTermVariations.isEmpty()) {
-            for (LoanTermVariations loanTermVariations : this.loanTermVariations) {
-                if (loanTermVariations.isActive()) {
-                    retData.add(loanTermVariations);
-                }
-            }
+    public List<LoanTermVariations> getActiveLoanTermVariations() {
+        if (this.loanTermVariations == null) {
+            return new ArrayList<>();
         }
-        return !retData.isEmpty() ? retData : null;
+
+        return this.loanTermVariations.stream().filter(LoanTermVariations::isActive).collect(Collectors.toList());
     }
 
     public boolean isTopup() {
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java
index 70af211d1..d667825e9 100644
--- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanTermVariationsRepository.java
@@ -19,6 +19,7 @@
 package org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain;
 
 import java.util.List;
+import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -28,7 +29,24 @@ import org.springframework.data.repository.query.Param;
 public interface LoanTermVariationsRepository
         extends JpaRepository<LoanTermVariations, Long>, JpaSpecificationExecutor<LoanTermVariations> {
 
-    @Query("select lrr from LoanTermVariations lrr where lrr.loan.id = :loanId")
-    List<LoanTermVariations> findAllLoanTermVariationsByLoanId(@Param("loanId") Long loanId);
+    @Query("""
+            select new org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData(
+                ltv.id, ltv.termType, ltv.termApplicableFrom, ltv.decimalValue, ltv.dateValue, ltv.isSpecificToInstallment
+            )
+            from LoanTermVariations ltv
+            where ltv.loan.id = :loanId
+            order by ltv.termApplicableFrom
+            """)
+    List<LoanTermVariationsData> findLoanTermVariationsByLoanId(@Param("loanId") long loanId);
+
+    @Query("""
+            select new org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData(
+                ltv.id, ltv.termType, ltv.termApplicableFrom, ltv.decimalValue, ltv.dateValue, ltv.isSpecificToInstallment
+            )
+            from LoanTermVariations ltv
+            where ltv.loan.id = :loanId and ltv.termType = :termType
+            order by ltv.termApplicableFrom
+            """)
+    List<LoanTermVariationsData> findLoanTermVariationsByLoanIdAndTermType(@Param("loanId") long loanId, @Param("termType") int termType);
 
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java
index 561d796f5..36cfe8637 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java
@@ -20,7 +20,6 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.
 
 import java.util.Collection;
 import java.util.List;
-import java.util.Set;
 import lombok.RequiredArgsConstructor;
 import org.apache.avro.generic.GenericContainer;
 import org.apache.commons.collections4.CollectionUtils;
@@ -86,9 +85,10 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer {
         List<LoanInstallmentDelinquencyBucketDataV1> installmentsDelinquencyData = installmentLevelDelinquencyEventProducer
                 .calculateInstallmentLevelDelinquencyData(event.get(), data.getCurrency());
 
-        Set<LoanTermVariations> activeLoanTermVariations = event.get().getActiveLoanTermVariations();
-        if (activeLoanTermVariations != null) {
-            data.setEmiAmountVariations(activeLoanTermVariations.stream().map(LoanTermVariations::toData).toList());
+        List<LoanTermVariations> activeLoanTermVariations = event.get().getActiveLoanTermVariations();
+
+        if (!activeLoanTermVariations.isEmpty()) {
+            data.setLoanTermVariations(activeLoanTermVariations.stream().map(LoanTermVariations::toData).toList());
         }
 
         LoanAccountDataV1 result = mapper.map(data);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java
index bbfff4ca7..6acd195b6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java
@@ -123,7 +123,7 @@ public class JobExecutionRepository {
 
     public Long getNotCompletedPartitionsCount(Long jobExecutionId, String partitionerStepName) {
         return namedParameterJdbcTemplate.queryForObject("""
-                    SELECT COUNT(bse.STEP_EXECUTION_ID)
+                    SELECT COUNT(BSE.STEP_EXECUTION_ID)
                     FROM BATCH_STEP_EXECUTION BSE
                     WHERE
                         BSE.JOB_EXECUTION_ID = :jobExecutionId
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 9ab94e1c9..c4d494910 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
@@ -146,6 +146,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
 import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
 import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
 import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService;
+import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
 import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService;
 import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
 import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
@@ -287,6 +288,7 @@ public class LoansApiResource {
     private final SqlValidator sqlValidator;
     private final LoanSummaryBalancesRepository loanSummaryBalancesRepository;
     private final ClientReadPlatformService clientReadPlatformService;
+    private final LoanTermVariationsRepository loanTermVariationsRepository;
 
     /*
      * This template API is used for loan approval, ideally this should be invoked on loan that are pending for
@@ -897,7 +899,8 @@ public class LoansApiResource {
         Collection<NoteData> notes = null;
         PortfolioAccountData linkedAccount = null;
         Collection<DisbursementData> disbursementData = null;
-        Collection<LoanTermVariationsData> emiAmountVariations = null;
+        List<LoanTermVariationsData> emiAmountVariations = null;
+        List<LoanTermVariationsData> loanTermVariations = null;
         Collection<LoanCollateralResponseData> loanCollateralManagements;
         Collection<LoanCollateralManagementData> loanCollateralManagementData = new ArrayList<>();
         CollectionData collectionData = this.delinquencyReadPlatformService.calculateLoanCollectionData(resolvedLoanId);
@@ -911,7 +914,8 @@ public class LoansApiResource {
                         DataTableApiConstant.transactionsAssociateParamName, DataTableApiConstant.chargesAssociateParamName,
                         DataTableApiConstant.guarantorsAssociateParamName, DataTableApiConstant.collateralAssociateParamName,
                         DataTableApiConstant.notesAssociateParamName, DataTableApiConstant.linkedAccountAssociateParamName,
-                        DataTableApiConstant.multiDisburseDetailsAssociateParamName, DataTableApiConstant.collectionAssociateParamName));
+                        DataTableApiConstant.multiDisburseDetailsAssociateParamName, DataTableApiConstant.collectionAssociateParamName,
+                        DataTableApiConstant.loanTermVariationsAssociateParamName));
             }
 
             ApiParameterHelper.excludeAssociationsForResponseIfProvided(exclude, associationParameters);
@@ -938,10 +942,15 @@ public class LoansApiResource {
             if (associationParameters.contains(DataTableApiConstant.emiAmountVariationsAssociateParamName)
                     || associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
                 mandatoryResponseParameters.add(DataTableApiConstant.emiAmountVariationsAssociateParamName);
-                emiAmountVariations = this.loanReadPlatformService.retrieveLoanTermVariations(resolvedLoanId,
+                emiAmountVariations = this.loanTermVariationsRepository.findLoanTermVariationsByLoanIdAndTermType(resolvedLoanId,
                         LoanTermVariationType.EMI_AMOUNT.getValue());
             }
 
+            if (associationParameters.contains(DataTableApiConstant.loanTermVariationsAssociateParamName)) {
+                mandatoryResponseParameters.add(DataTableApiConstant.loanTermVariationsAssociateParamName);
+                loanTermVariations = this.loanTermVariationsRepository.findLoanTermVariationsByLoanId(resolvedLoanId);
+            }
+
             if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) {
                 mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName);
                 final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData(
@@ -1118,7 +1127,8 @@ public class LoansApiResource {
                 interestCalculationPeriodTypeOptions, fundOptions, chargeOptions, chargeTemplate, allowedLoanOfficers, loanPurposeOptions,
                 loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations,
                 overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, collectionData,
-                LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList());
+                LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(),
+                loanTermVariations);
 
         final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(),
                 mandatoryResponseParameters);
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 39a4603ba..5b1b8e9bb 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
@@ -432,11 +432,6 @@ final class LoansApiResourceSwagger {
 
             private GetLoansLoanIdSummary() {}
 
-            static final class GetLoansLoanIdEmiVariations {
-
-                private GetLoansLoanIdEmiVariations() {}
-            }
-
             static final class GetLoansLoanIdLinkedAccount {
 
                 private GetLoansLoanIdLinkedAccount() {}
@@ -606,7 +601,6 @@ final class LoansApiResourceSwagger {
             public Double maxOutstandingLoanBalance;
             @Schema(example = "false")
             public Boolean canDisburse;
-            public Set<GetLoansLoanIdEmiVariations> emiAmountVariations;
             @Schema(example = "true")
             public Boolean inArrears;
             @Schema(example = "false")
@@ -1072,6 +1066,38 @@ final class LoansApiResourceSwagger {
 
         }
 
+        static final class GetLoansLoanIdLoanTermVariations {
+
+            private GetLoansLoanIdLoanTermVariations() {}
+
+            static final class GetLoansLoanIdLoanTermEnumData {
+
+                private GetLoansLoanIdLoanTermEnumData() {}
+
+                @Schema(example = "1")
+                public Long id;
+                @Schema(example = "loanTermType.emiAmount")
+                public String code;
+                @Schema(example = "emiAmount")
+                public String value;
+            }
+
+            @Schema(example = "1")
+            public Long id;
+            @Schema(description = "Enum option data")
+            public GetLoansLoanIdLoanTermEnumData termType;
+            @Schema(example = "[2024, 1, 1]")
+            public LocalDate termVariationApplicableFrom;
+            @Schema(example = "200.000000")
+            public Double decimalValue;
+            @Schema(example = "[2024, 1, 1]")
+            public LocalDate dateValue;
+            @Schema(example = "false")
+            public boolean isSpecificToInstallment;
+            @Schema(example = "false")
+            public boolean isProcessed;
+        }
+
         @Schema(example = "1")
         public Long id;
         @Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
@@ -1172,6 +1198,10 @@ final class LoansApiResourceSwagger {
         public EnumOptionData loanScheduleType;
         @Schema(example = "HORIZONTAL")
         public EnumOptionData loanScheduleProcessingType;
+        @Schema(description = "List of GetLoansLoanIdLoanTermVariations")
+        public List<GetLoansLoanIdLoanTermVariations> emiAmountVariations;
+        @Schema(description = "List of GetLoansLoanIdLoanTermVariations")
+        public List<GetLoansLoanIdLoanTermVariations> loanTermVariations;
     }
 
     @Schema(description = "GetLoansResponse")
@@ -1267,6 +1297,8 @@ final class LoansApiResourceSwagger {
         public BigDecimal disbursedAmountPercentageForDownPayment;
         @Schema(example = "false")
         public Boolean enableAutoRepaymentForDownPayment;
+        @Schema(example = "10.00")
+        public BigDecimal fixedEmiAmount;
 
         public List<PostLoansRequestChargeData> charges;
 
@@ -1588,6 +1620,10 @@ final class LoansApiResourceSwagger {
         public String withdrawnOnDate;
         @Schema(description = "List of PostLoansLoanIdDisbursementData")
         public List<PostLoansLoanIdDisbursementData> disbursementData;
+        @Schema(example = "500.00")
+        public BigDecimal fixedEmiAmount;
+        @Schema(example = "28 July 2022")
+        public String adjustRepaymentDate;
     }
 
     @Schema(description = "PostLoansLoanIdResponse")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
index 8b382aacd..1b87b9559 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java
@@ -194,6 +194,7 @@ public class LoanAccountData {
     private Boolean canDisburse;
 
     private Collection<LoanTermVariationsData> emiAmountVariations;
+    private Collection<LoanTermVariationsData> loanTermVariations;
     private Collection<LoanAccountSummaryData> clientActiveLoanOptions;
     private Boolean canUseForTopup;
     // TODO: avoid prefix "is"
@@ -521,7 +522,7 @@ public class LoanAccountData {
             final PaidInAdvanceData paidInAdvance, Collection<InterestRatePeriodData> interestRatesPeriods,
             final Collection<LoanAccountSummaryData> clientActiveLoanOptions, final List<RateData> rates, final Boolean isRatesEnabled,
             final CollectionData delinquent, final List<EnumOptionData> loanScheduleTypeOptions,
-            final List<EnumOptionData> loanScheduleProcessingTypeOptions) {
+            final List<EnumOptionData> loanScheduleProcessingTypeOptions, final List<LoanTermVariationsData> loanTermVariations) {
 
         // TODO: why are these variables 'calendarData', 'chargeTemplate' never used (see original private constructor)
 
@@ -541,7 +542,7 @@ public class LoanAccountData {
                 .setOverdueCharges(overdueCharges).setPaidInAdvance(paidInAdvance).setInterestRatesPeriods(interestRatesPeriods)
                 .setClientActiveLoanOptions(clientActiveLoanOptions).setRates(rates).setIsRatesEnabled(isRatesEnabled)
                 .setDelinquent(delinquent).setLoanScheduleTypeOptions(loanScheduleTypeOptions)
-                .setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions);
+                .setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions).setLoanTermVariations(loanTermVariations);
     }
 
     public LoanAccountData associationsAndTemplate(final Collection<LoanProductData> productOptions,
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java
index 235155aaa..24e0befbf 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java
@@ -370,9 +370,9 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
             final LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(scheduleGeneratorDTO);
 
             LocalDate rescheduleFromDate = null;
-            Set<LoanTermVariations> activeLoanTermVariations = loan.getActiveLoanTermVariations();
+            List<LoanTermVariations> activeLoanTermVariations = loan.getActiveLoanTermVariations();
             LoanTermVariations dueDateVariationInCurrentRequest = loanRescheduleRequest.getDueDateTermVariationIfExists();
-            if (dueDateVariationInCurrentRequest != null && activeLoanTermVariations != null) {
+            if (dueDateVariationInCurrentRequest != null && !activeLoanTermVariations.isEmpty()) {
                 LocalDate fromScheduleDate = dueDateVariationInCurrentRequest.fetchTermApplicaDate();
                 LocalDate currentScheduleDate = fromScheduleDate;
                 LocalDate modifiedScheduleDate = dueDateVariationInCurrentRequest.fetchDateValue();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
index 9d626b4d5..8f39c262e 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
@@ -32,7 +32,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
-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;
@@ -99,8 +98,6 @@ public interface LoanReadPlatformService {
 
     DisbursementData retrieveLoanDisbursementDetail(Long loanId, Long disbursementId);
 
-    Collection<LoanTermVariationsData> retrieveLoanTermVariations(Long loanId, Integer termType);
-
     Collection<LoanScheduleAccrualData> retriveScheduleAccrualData();
 
     LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index 4e38213f2..c80f79d89 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -100,7 +100,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInsta
 import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
-import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionRelationData;
@@ -114,7 +113,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTra
 import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
-import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
@@ -1740,33 +1738,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
         return this.jdbcTemplate.queryForObject(sql, rm, loanId, disbursementId); // NOSONAR
     }
 
-    @Override
-    public Collection<LoanTermVariationsData> retrieveLoanTermVariations(Long loanId, Integer termType) {
-        final LoanTermVariationsMapper rm = new LoanTermVariationsMapper();
-        final String sql = "select " + rm.schema() + " where tv.loan_id=? and tv.term_type=?";
-        return this.jdbcTemplate.query(sql, rm, loanId, termType); // NOSONAR
-    }
-
-    private static final class LoanTermVariationsMapper implements RowMapper<LoanTermVariationsData> {
-
-        public String schema() {
-            return "tv.id as id,tv.applicable_date as variationApplicableFrom,tv.decimal_value as decimalValue, tv.date_value as dateValue, tv.is_specific_to_installment as isSpecificToInstallment "
-                    + "from m_loan_term_variations tv";
-        }
-
-        @Override
-        public LoanTermVariationsData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
-            final Long id = rs.getLong("id");
-            final LocalDate variationApplicableFrom = JdbcSupport.getLocalDate(rs, "variationApplicableFrom");
-            final BigDecimal decimalValue = rs.getBigDecimal("decimalValue");
-            final LocalDate dateValue = JdbcSupport.getLocalDate(rs, "dateValue");
-            final boolean isSpecificToInstallment = rs.getBoolean("isSpecificToInstallment");
-
-            return new LoanTermVariationsData(id, LoanEnumerations.loanVariationType(LoanTermVariationType.EMI_AMOUNT),
-                    variationApplicableFrom, decimalValue, dateValue, isSpecificToInstallment);
-        }
-    }
-
     @Override
     public Collection<LoanScheduleAccrualData> retriveScheduleAccrualData() {
         final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
index 5b4ebbecb..cdba5ea62 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
@@ -94,6 +94,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.integrationtests.common.BusinessDateHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
+import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
 import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.accounting.Account;
@@ -176,6 +177,8 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
     private static final BusinessDateHelper BUSINESS_DATE_HELPER = new BusinessDateHelper();
     private static final ChargesHelper CHARGES_HELPER = new ChargesHelper();
     private static final ClientHelper CLIENT_HELPER = new ClientHelper(REQUEST_SPEC, RESPONSE_SPEC);
+    private static final LoanRescheduleRequestHelper LOAN_RESCHEDULE_REQUEST_HELPER = new LoanRescheduleRequestHelper(REQUEST_SPEC,
+            RESPONSE_SPEC);
 
     private static RequestSpecification createRequestSpecification() {
         RequestSpecification request = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
index 10f5d3b9a..33f4fbe8e 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
@@ -290,30 +290,23 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
                         && Objects.equals(statusId, businessEvent.getStatusId().doubleValue())
                         && Objects.equals(principalDisbursed, businessEvent.getPrincipalDisbursed())
                         && Objects.equals(principalOutstanding, businessEvent.getPrincipalOutstanding())
-                        && emiAmountVariationsMatch((List<Map<String, Object>>) externalEvent.getPayLoad().get("emiAmountVariations"),
-                                businessEvent.emiAmountVariationType);
+                        && loanTermVariationsMatch((List<Map<String, Object>>) externalEvent.getPayLoad().get("loanTermVariations"),
+                                businessEvent.loanTermVariationType);
             }).count();
             Assertions.assertEquals(1, count, "Expected business event not found " + businessEvent);
         }
     }
 
-    private boolean emiAmountVariationsMatch(List<Map<String, Object>> emiAmountVariations, List<String> expectedTypes) {
+    private boolean loanTermVariationsMatch(final List<Map<String, Object>> loanTermVariations, final List<String> expectedTypes) {
         if (CollectionUtils.isEmpty(expectedTypes)) {
             return true;
         }
-        int numberOfMatches = 0;
-        if (CollectionUtils.isNotEmpty(emiAmountVariations)) {
-            for (String expectedType : expectedTypes) {
-                int i = 0;
-                while (i < emiAmountVariations.size() && !StringUtils
-                        .equals((String) ((Map<String, Object>) emiAmountVariations.get(i).get("termType")).get("value"), expectedType)) {
-                    i++;
-                }
-                if (i < emiAmountVariations.size()) {
-                    numberOfMatches++;
-                }
-            }
-        }
+        final long numberOfMatches = expectedTypes
+                .stream().filter(
+                        expectedType -> loanTermVariations.stream()
+                                .anyMatch(variation -> StringUtils
+                                        .equals((String) ((Map<String, Object>) variation.get("termType")).get("value"), expectedType)))
+                .count();
 
         return numberOfMatches == expectedTypes.size();
     }
@@ -335,6 +328,6 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
         private Integer statusId;
         private Double principalDisbursed;
         private Double principalOutstanding;
-        private List<String> emiAmountVariationType;
+        private List<String> loanTermVariationType;
     }
 }