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 <[email protected]>
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;
     }
 }

Reply via email to