From 25866edfc93827b65ca3137a6ce32a5b772fe0c1 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Thu, 3 Oct 2024 14:21:01 +0300 Subject: [PATCH] FINERACT-2081: Add all loan term validations to loan details --- .../main/avro/loan/v1/LoanAccountDataV1.avsc | 11 + .../dataqueries/api/DataTableApiConstant.java | 1 + .../test/helper/ErrorMessageHelper.java | 15 + .../test/stepdef/loan/LoanStepDef.java | 294 +++++++++++++----- .../src/test/resources/features/Loan.feature | 29 ++ .../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 +- .../api/LoansApiResourceSwagger.java | 48 ++- .../loanaccount/data/LoanAccountData.java | 5 +- ...heduleRequestWritePlatformServiceImpl.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> actual, + final List 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> data = table.asLists(); - List 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); + public void createFullyCustomizedLoan(final DataTable table) throws IOException { + final List> data = table.asLists(); + createCustomizedLoan(data.get(1), false); + } - Response 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 response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute(); - testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response); - ErrorHelper.checkSuccessfulApiCall(response); - - 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> 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 loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); - long loanId = loanResponse.body().getLoanId(); - PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest() + final Response 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 loanDisburseResponse = loansApi.stateTransitions(loanId, disburseRequest, "disburse").execute(); - testContext().set(TestContextKey.LOAN_DISBURSE_RESPONSE, loanDisburseResponse); - ErrorHelper.checkSuccessfulApiCall(loanDisburseResponse); - Long statusActual = loanDisburseResponse.body().getChanges().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 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); + } - Response loanDetails = loansApi.retrieveLoan(loanId, false, "", "", "").execute(); - 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); + @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 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 loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + assertNotNull(loanCreateResponse.body()); + final long loanId = loanCreateResponse.body().getLoanId(); + + final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "all", "", "").execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + final List emiAmountVariations = loanDetailsResponse.body().getEmiAmountVariations(); + + final List> data = table.asLists(); + assertNotNull(emiAmountVariations); + final int linesActual = emiAmountVariations.size(); + data.stream().skip(1) // skip headers + .forEach(expectedValues -> { + final List> 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 loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + assertNotNull(loanCreateResponse.body()); + final long loanId = loanCreateResponse.body().getLoanId(); + + final Response loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "loanTermVariations", "", "") + .execute(); + ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); + + final List loanTermVariations = loanDetailsResponse.body().getLoanTermVariations(); + assertNotNull(loanTermVariations); + + final List> data = table.asLists(); + final int linesActual = loanTermVariations.size(); + data.stream().skip(1) // skip headers + .forEach(expectedValues -> { + final String expectedTermTypeId = expectedValues.get(0); + + final List> 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 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 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 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 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 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 fetchValuesOfLoanTermVariations(final List header, final GetLoansLoanIdLoanTermVariations emiVariation) { + final List 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 { @@ -46,6 +47,16 @@ public class LoanTermVariationsData implements Comparable { return isForeClosure; } - public Set getActiveLoanTermVariations() { - Set retData = new HashSet<>(); - if (this.loanTermVariations != null && !this.loanTermVariations.isEmpty()) { - for (LoanTermVariations loanTermVariations : this.loanTermVariations) { - if (loanTermVariations.isActive()) { - retData.add(loanTermVariations); - } - } + public List 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, JpaSpecificationExecutor { - @Query("select lrr from LoanTermVariations lrr where lrr.loan.id = :loanId") - List 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 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 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 installmentsDelinquencyData = installmentLevelDelinquencyEventProducer .calculateInstallmentLevelDelinquencyData(event.get(), data.getCurrency()); - Set activeLoanTermVariations = event.get().getActiveLoanTermVariations(); - if (activeLoanTermVariations != null) { - data.setEmiAmountVariations(activeLoanTermVariations.stream().map(LoanTermVariations::toData).toList()); + List 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 notes = null; PortfolioAccountData linkedAccount = null; Collection disbursementData = null; - Collection emiAmountVariations = null; + List emiAmountVariations = null; + List loanTermVariations = null; Collection loanCollateralManagements; Collection 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 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 emiAmountVariations; + @Schema(description = "List of GetLoansLoanIdLoanTermVariations") + public List 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 charges; @@ -1588,6 +1620,10 @@ final class LoansApiResourceSwagger { public String withdrawnOnDate; @Schema(description = "List of PostLoansLoanIdDisbursementData") public List 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 emiAmountVariations; + private Collection loanTermVariations; private Collection clientActiveLoanOptions; private Boolean canUseForTopup; // TODO: avoid prefix "is" @@ -521,7 +522,7 @@ public class LoanAccountData { final PaidInAdvanceData paidInAdvance, Collection interestRatesPeriods, final Collection clientActiveLoanOptions, final List rates, final Boolean isRatesEnabled, final CollectionData delinquent, final List loanScheduleTypeOptions, - final List loanScheduleProcessingTypeOptions) { + final List loanScheduleProcessingTypeOptions, final List 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 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 activeLoanTermVariations = loan.getActiveLoanTermVariations(); + List 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 retrieveLoanTermVariations(Long loanId, Integer termType); - Collection 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 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 { - - 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 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>) externalEvent.getPayLoad().get("emiAmountVariations"), - businessEvent.emiAmountVariationType); + && loanTermVariationsMatch((List>) externalEvent.getPayLoad().get("loanTermVariations"), + businessEvent.loanTermVariationType); }).count(); Assertions.assertEquals(1, count, "Expected business event not found " + businessEvent); } } - private boolean emiAmountVariationsMatch(List> emiAmountVariations, List expectedTypes) { + private boolean loanTermVariationsMatch(final List> loanTermVariations, final List 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) 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) 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 emiAmountVariationType; + private List loanTermVariationType; } }