You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2025/10/29 10:26:22 UTC
(fineract) branch develop updated (b32837e9b4 -> 8a86759773)
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a change to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
from b32837e9b4 FINERACT-2390: Fix `infinite loop` issue
new adc4ee3545 FINERACT-2389: Fix the handling of nullable field overrides
new 8a86759773 FINERACT-2389: Fix the handling of nullable field overrides - E2E tests
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../test/data/loanproduct/DefaultLoanProduct.java | 2 +
.../global/LoanProductGlobalInitializerStep.java | 49 +++++++++
.../stepdef/loan/LoanOverrideFieldsStepDef.java | 120 +++++++++++++++++++++
.../fineract/test/stepdef/loan/LoanStepDef.java | 80 ++++++++++++++
.../fineract/test/support/TestContextKey.java | 2 +
.../resources/features/LoanDelinquency.feature | 32 ++++++
.../resources/features/LoanOverrideFileds.feature | 62 +++++++++++
.../loanaccount/api/LoansApiResourceSwagger.java | 10 ++
.../service/LoanScheduleAssembler.java | 30 +++---
.../integrationtests/BaseLoanIntegrationTest.java | 30 ++++--
.../DelinquencyActionIntegrationTests.java | 10 +-
.../common/loans/LoanProductTestBuilder.java | 4 +-
.../InitiateExternalAssetOwnerTransferTest.java | 4 +-
13 files changed, 406 insertions(+), 29 deletions(-)
create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOverrideFieldsStepDef.java
create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature
(fineract) 01/02: FINERACT-2389: Fix the handling of nullable field overrides
Posted by ad...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
commit adc4ee3545c2a409ae8c9d52129cfcf73dde7e26
Author: Oleksii Novikov <al...@onix-systems.com>
AuthorDate: Tue Oct 21 15:41:36 2025 +0300
FINERACT-2389: Fix the handling of nullable field overrides
---
.../test/data/loanproduct/DefaultLoanProduct.java | 2 +
.../global/LoanProductGlobalInitializerStep.java | 49 +++++++++
.../stepdef/loan/LoanOverrideFieldsStepDef.java | 120 +++++++++++++++++++++
.../fineract/test/support/TestContextKey.java | 2 +
.../resources/features/LoanOverrideFileds.feature | 58 ++++++++++
.../loanaccount/api/LoansApiResourceSwagger.java | 10 ++
.../service/LoanScheduleAssembler.java | 30 +++---
.../integrationtests/BaseLoanIntegrationTest.java | 30 ++++--
.../DelinquencyActionIntegrationTests.java | 10 +-
.../common/loans/LoanProductTestBuilder.java | 4 +-
.../InitiateExternalAssetOwnerTransferTest.java | 4 +-
11 files changed, 290 insertions(+), 29 deletions(-)
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
index 1fe4813d90..851365f5a5 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
@@ -170,6 +170,8 @@ public enum DefaultLoanProduct implements LoanProduct {
LP1_INTEREST_FLAT_DAILY_RECALCULATION_DAILY_360_30_MULTIDISB, //
LP1_INTEREST_FLAT_SAR_RECALCULATION_SAME_AS_REPAYMENT_MULTIDISB_AUTO_DOWNPAYMENT, //
LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY, //
+ LP1_WITH_OVERRIDES, //
+ LP1_NO_OVERRIDES //
;
@Override
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
index 72f6100135..f9251b541b 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
@@ -36,6 +36,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.client.models.AdvancedPaymentData;
+import org.apache.fineract.client.models.AllowAttributeOverrides;
import org.apache.fineract.client.models.CreditAllocationData;
import org.apache.fineract.client.models.CreditAllocationOrder;
import org.apache.fineract.client.models.LoanProductChargeData;
@@ -4056,6 +4057,54 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY,
responseLoanProductsResponseAdvCustomPaymentAllocationProgressiveLoanInterestDailyEmi36030InterestRecalculationDaily);
+
+ // (LP1_WITH_OVERRIDES) - Loan product with all attribute overrides ENABLED
+ final String nameWithOverrides = DefaultLoanProduct.LP1_WITH_OVERRIDES.getName();
+ final PostLoanProductsRequest loanProductsRequestWithOverrides = loanProductsRequestFactory.defaultLoanProductsRequestLP1() //
+ .name(nameWithOverrides) //
+ .interestRatePerPeriod(1.0) //
+ .maxInterestRatePerPeriod(30.0) //
+ .inArrearsTolerance(10) //
+ .graceOnPrincipalPayment(1) //
+ .graceOnInterestPayment(1) //
+ .graceOnArrearsAgeing(3) //
+ .numberOfRepayments(6) //
+ .allowAttributeOverrides(new AllowAttributeOverrides() //
+ .amortizationType(true) //
+ .interestType(true) //
+ .transactionProcessingStrategyCode(true) //
+ .interestCalculationPeriodType(true) //
+ .inArrearsTolerance(true) //
+ .repaymentEvery(true) //
+ .graceOnPrincipalAndInterestPayment(true) //
+ .graceOnArrearsAgeing(true));
+ final Response<PostLoanProductsResponse> responseWithOverrides = loanProductsApi.createLoanProduct(loanProductsRequestWithOverrides)
+ .execute();
+ TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_WITH_OVERRIDES, responseWithOverrides);
+
+ // (LP1_NO_OVERRIDES) - Loan product with all attribute overrides DISABLED
+ final String nameNoOverrides = DefaultLoanProduct.LP1_NO_OVERRIDES.getName();
+ final PostLoanProductsRequest loanProductsRequestNoOverrides = loanProductsRequestFactory.defaultLoanProductsRequestLP1() //
+ .name(nameNoOverrides) //
+ .interestRatePerPeriod(1.0) //
+ .maxInterestRatePerPeriod(30.0) //
+ .inArrearsTolerance(10) //
+ .graceOnPrincipalPayment(1) //
+ .graceOnInterestPayment(1) //
+ .graceOnArrearsAgeing(3) //
+ .numberOfRepayments(6) //
+ .allowAttributeOverrides(new AllowAttributeOverrides() //
+ .amortizationType(false) //
+ .interestType(false) //
+ .transactionProcessingStrategyCode(false) //
+ .interestCalculationPeriodType(false) //
+ .inArrearsTolerance(false) //
+ .repaymentEvery(false) //
+ .graceOnPrincipalAndInterestPayment(false) //
+ .graceOnArrearsAgeing(false));
+ final Response<PostLoanProductsResponse> responseNoOverrides = loanProductsApi.createLoanProduct(loanProductsRequestNoOverrides)
+ .execute();
+ TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_NO_OVERRIDES, responseNoOverrides);
}
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOverrideFieldsStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOverrideFieldsStepDef.java
new file mode 100644
index 0000000000..4f8185c9bb
--- /dev/null
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOverrideFieldsStepDef.java
@@ -0,0 +1,120 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.test.stepdef.loan;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import io.cucumber.datatable.DataTable;
+import io.cucumber.java.en.Then;
+import io.cucumber.java.en.When;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.client.services.LoansApi;
+import org.apache.fineract.test.data.loanproduct.DefaultLoanProduct;
+import org.apache.fineract.test.data.loanproduct.LoanProductResolver;
+import org.apache.fineract.test.factory.LoanRequestFactory;
+import org.apache.fineract.test.helper.ErrorHelper;
+import org.apache.fineract.test.stepdef.AbstractStepDef;
+import org.apache.fineract.test.support.TestContextKey;
+import retrofit2.Response;
+
+@RequiredArgsConstructor
+public class LoanOverrideFieldsStepDef extends AbstractStepDef {
+
+ private final LoanRequestFactory loanRequestFactory;
+ private final LoanProductResolver loanProductResolver;
+ private final LoansApi loansApi;
+
+ @Then("LoanDetails has {string} field with value: {string}")
+ public void checkLoanDetailsFieldWithValue(final String fieldName, final String expectedValue) throws IOException {
+ final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+ assertNotNull(loanResponse.body());
+ final Long loanId = loanResponse.body().getLoanId();
+
+ final Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "", "", "").execute();
+ ErrorHelper.checkSuccessfulApiCall(loanDetails);
+ assertNotNull(loanDetails.body());
+
+ verifyFieldValue(loanDetails.body(), fieldName, expectedValue);
+ }
+
+ private void verifyFieldValue(final GetLoansLoanIdResponse loanDetails, final String fieldName, final String expectedValue) {
+ final Integer actualValue = getIntFieldValue(loanDetails, fieldName);
+ final Integer expected = Integer.valueOf(expectedValue);
+ assertThat(actualValue).as("Expected %s to be %d but was %s", fieldName, expected, actualValue).isEqualTo(expected);
+ }
+
+ private Integer getIntFieldValue(final GetLoansLoanIdResponse loanDetails, final String fieldName) {
+ return switch (fieldName) {
+ case "inArrearsTolerance" -> loanDetails.getInArrearsTolerance();
+ case "graceOnPrincipalPayment" -> loanDetails.getGraceOnPrincipalPayment();
+ case "graceOnInterestPayment" -> loanDetails.getGraceOnInterestPayment();
+ case "graceOnArrearsAgeing" -> loanDetails.getGraceOnArrearsAgeing();
+ default -> throw new IllegalArgumentException("Unknown override field: " + fieldName);
+ };
+ }
+
+ @When("Admin creates a new Loan with the following override data:")
+ public void createLoanWithOverrideData(final DataTable dataTable) throws IOException {
+ final Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
+ assertNotNull(clientResponse.body());
+ final Long clientId = clientResponse.body().getClientId();
+
+ final Map<String, String> overrideData = dataTable.asMap(String.class, String.class);
+
+ final String loanProductName = overrideData.get("loanProduct");
+ if (loanProductName == null) {
+ throw new IllegalArgumentException("loanProduct is required in override data");
+ }
+
+ final PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId)
+ .productId(loanProductResolver.resolve(DefaultLoanProduct.valueOf(loanProductName))).numberOfRepayments(6)
+ .loanTermFrequency(180).interestRatePerPeriod(new BigDecimal(1));
+
+ overrideData.forEach((fieldName, value) -> {
+ if (!"loanProduct".equals(fieldName)) {
+ applyOverrideField(loansRequest, fieldName, value);
+ }
+ });
+
+ final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
+ testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
+ ErrorHelper.checkSuccessfulApiCall(response);
+ }
+
+ private void applyOverrideField(final PostLoansRequest request, final String fieldName, final String value) {
+ final boolean isNull = "null".equals(value);
+
+ switch (fieldName) {
+ case "inArrearsTolerance" -> request.inArrearsTolerance(isNull ? null : new BigDecimal(value));
+ case "graceOnInterestPayment" -> request.graceOnInterestPayment(isNull ? null : Integer.valueOf(value));
+ case "graceOnPrincipalPayment" -> request.graceOnPrincipalPayment(isNull ? null : Integer.valueOf(value));
+ case "graceOnArrearsAgeing" -> request.graceOnArrearsAgeing(isNull ? null : Integer.valueOf(value));
+ default -> throw new IllegalArgumentException("Unknown override field: " + fieldName);
+ }
+ }
+
+}
diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index 37c72d2d34..0f56c55464 100644
--- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -275,4 +275,6 @@ public abstract class TestContextKey {
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentInterestRecalculationMultidisbursalApprovedOverAppliedAmountExpectedTransches";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_MIN_INT_3_MAX_INT_20 = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyTillPreCloseMinInt3MaxInt20";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_WRITE_OFF_REASON_MAP = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentWriteOffReasonMap";
+ public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_WITH_OVERRIDES = "loanProductCreateResponseLP1WithOverrides";
+ public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_NO_OVERRIDES = "loanProductCreateResponseLP1NoOverrides";
}
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature
new file mode 100644
index 0000000000..b955d0f272
--- /dev/null
+++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature
@@ -0,0 +1,58 @@
+@LoanOverrideFields
+Feature: LoanOverrideFields
+
+ Scenario: Verify that all nullable fields default to product when overrides not allowed and not provided
+ When Admin sets the business date to the actual date
+ When Admin creates a client with random data
+ When Admin creates a new Loan with the following override data:
+ | loanProduct | LP1_NO_OVERRIDES |
+ | inArrearsTolerance | null |
+ | graceOnPrincipalPayment | null |
+ | graceOnInterestPayment | null |
+ | graceOnArrearsAgeing | null |
+ Then LoanDetails has "inArrearsTolerance" field with value: "10"
+ Then LoanDetails has "graceOnPrincipalPayment" field with value: "1"
+ Then LoanDetails has "graceOnInterestPayment" field with value: "1"
+ Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
+
+ Scenario: Verify that all nullable fields ignore overrides when overrides not allowed
+ When Admin sets the business date to the actual date
+ When Admin creates a client with random data
+ When Admin creates a new Loan with the following override data:
+ | loanProduct | LP1_NO_OVERRIDES |
+ | inArrearsTolerance | 11 |
+ | graceOnPrincipalPayment | 2 |
+ | graceOnInterestPayment | 2 |
+ | graceOnArrearsAgeing | 4 |
+ Then LoanDetails has "inArrearsTolerance" field with value: "10"
+ Then LoanDetails has "graceOnPrincipalPayment" field with value: "1"
+ Then LoanDetails has "graceOnInterestPayment" field with value: "1"
+ Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
+
+ Scenario: Verify that nullable fields default to product when override is allowed but not provided
+ When Admin sets the business date to the actual date
+ When Admin creates a client with random data
+ When Admin creates a new Loan with the following override data:
+ | loanProduct | LP1_WITH_OVERRIDES |
+ | inArrearsTolerance | null |
+ | graceOnPrincipalPayment | null |
+ | graceOnInterestPayment | null |
+ | graceOnArrearsAgeing | null |
+ Then LoanDetails has "inArrearsTolerance" field with value: "10"
+ Then LoanDetails has "graceOnPrincipalPayment" field with value: "1"
+ Then LoanDetails has "graceOnInterestPayment" field with value: "1"
+ Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
+
+ Scenario: Verify that nullable fields default to product when override is allowed and provided
+ When Admin sets the business date to the actual date
+ When Admin creates a client with random data
+ When Admin creates a new Loan with the following override data:
+ | loanProduct | LP1_WITH_OVERRIDES |
+ | inArrearsTolerance | 11 |
+ | graceOnPrincipalPayment | 2 |
+ | graceOnInterestPayment | 2 |
+ | graceOnArrearsAgeing | 4 |
+ Then LoanDetails has "inArrearsTolerance" field with value: "11"
+ Then LoanDetails has "graceOnPrincipalPayment" field with value: "2"
+ Then LoanDetails has "graceOnInterestPayment" field with value: "2"
+ Then LoanDetails has "graceOnArrearsAgeing" field with value: "4"
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 3ca327b710..40d97e3fc4 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
@@ -1216,6 +1216,14 @@ final class LoansApiResourceSwagger {
public Boolean chargedOff;
@Schema(example = "3")
public Integer inArrearsTolerance;
+ @Schema(example = "0")
+ public Integer graceOnPrincipalPayment;
+ @Schema(example = "0")
+ public Integer graceOnInterestPayment;
+ @Schema(example = "0")
+ public Integer graceOnInterestCharged;
+ @Schema(example = "3")
+ public Integer graceOnArrearsAgeing;
@Schema(example = "false")
public Boolean enableDownPayment;
@Schema(example = "0.000000")
@@ -1348,6 +1356,8 @@ final class LoansApiResourceSwagger {
public Integer graceOnInterestPayment;
@Schema(example = "1")
public Integer graceOnArrearsAgeing;
+ @Schema(example = "10")
+ public BigDecimal inArrearsTolerance;
@Schema(example = "HORIZONTAL")
public String loanScheduleProcessingType;
@Schema(example = "false")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
index c0742533d9..bb303f52b6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
@@ -350,27 +350,33 @@ public class LoanScheduleAssembler {
}
// grace details
- final Integer graceOnPrincipalPayment = allowOverridingGraceOnPrincipalAndInterestPayment
- ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element)
- : loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment();
+ Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element);
+ if (!allowOverridingGraceOnPrincipalAndInterestPayment || graceOnPrincipalPayment == null) {
+ graceOnPrincipalPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment();
+ }
final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper
.extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element);
- final Integer graceOnInterestPayment = allowOverridingGraceOnPrincipalAndInterestPayment
- ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element)
- : loanProduct.getLoanProductRelatedDetail().getGraceOnInterestPayment();
+ Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element);
+ if (!allowOverridingGraceOnPrincipalAndInterestPayment || graceOnInterestPayment == null) {
+ graceOnInterestPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnInterestPayment();
+ }
final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element);
final LocalDate interestChargedFromDate = this.fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element);
final Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = this.configurationDomainService
.isInterestChargedFromDateSameAsDisbursementDate();
- final Integer graceOnArrearsAgeing = allowOverridingGraceOnArrearsAging
- ? this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element)
- : loanProduct.getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
+ Integer graceOnArrearsAgeing = this.fromApiJsonHelper
+ .extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element);
+ if (!allowOverridingGraceOnArrearsAging || graceOnArrearsAgeing == null) {
+ graceOnArrearsAgeing = loanProduct.getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
+ }
// other
- final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
- final Money inArrearsToleranceMoney = allowOverridingArrearsTolerance ? Money.of(currency, inArrearsTolerance)
- : loanProduct.getLoanProductRelatedDetail().getInArrearsTolerance();
+ BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
+ if (!allowOverridingArrearsTolerance || inArrearsTolerance == null) {
+ inArrearsTolerance = loanProduct.getLoanProductRelatedDetail().getInArrearsTolerance().getAmount();
+ }
+ final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance);
final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName,
element);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index 45280c101f..e8daf29065 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -886,7 +886,8 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
.interestType(interestType)//
- .amortizationType(amortizationType);
+ .amortizationType(amortizationType)//
+ .graceOnArrearsAgeing(0);
}
private RequestSpecification createRequestSpecification(String authKey) {
@@ -1352,13 +1353,26 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
protected PostLoansRequest applyLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
- PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId).productId(loanProductId)
- .expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN)
- .transactionProcessingStrategyCode(DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY)
- .locale("en").submittedOnDate(loanDisbursementDate).amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO)
- .interestCalculationPeriodType(1).interestType(0).repaymentEvery(30).repaymentFrequencyType(0)
- .numberOfRepayments(numberOfRepayments).loanTermFrequency(numberOfRepayments * 30).loanTermFrequencyType(0)
- .maxOutstandingLoanBalance(BigDecimal.valueOf(amount)).principal(BigDecimal.valueOf(amount)).loanType("individual");
+ PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId) //
+ .productId(loanProductId) //
+ .expectedDisbursementDate(loanDisbursementDate) //
+ .dateFormat(DATETIME_PATTERN) //
+ .transactionProcessingStrategyCode(DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY) //
+ .locale("en") //
+ .submittedOnDate(loanDisbursementDate) //
+ .amortizationType(1) //
+ .interestRatePerPeriod(BigDecimal.ZERO) //
+ .interestCalculationPeriodType(1) //
+ .interestType(0) //
+ .repaymentEvery(30) //
+ .repaymentFrequencyType(0) //
+ .numberOfRepayments(numberOfRepayments) //
+ .loanTermFrequency(numberOfRepayments * 30) //
+ .loanTermFrequencyType(0) //
+ .maxOutstandingLoanBalance(BigDecimal.valueOf(amount)) //
+ .principal(BigDecimal.valueOf(amount)) //
+ .loanType("individual") //
+ .graceOnArrearsAgeing(0);
if (customizer != null) {
customizer.accept(postLoansRequest);
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
index 168a029c8b..12be1252a1 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
@@ -285,15 +285,15 @@ public class DelinquencyActionIntegrationTests extends BaseLoanIntegrationTest {
verifyLoanDelinquencyData(loanId, 6, new InstallmentDelinquencyData(4, 10, BigDecimal.valueOf(250.0)));
// Create Delinquency Pause for the Loan in the past
- PostLoansDelinquencyActionResponse response = loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE,
- "27 January 2023", "15 February 2023");
+ loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "27 January 2023", "15 February 2023");
List<GetDelinquencyActionsResponse> loanDelinquencyActions = loanTransactionHelper.getLoanDelinquencyActions(loanId);
Assertions.assertNotNull(loanDelinquencyActions);
Assertions.assertEquals(1, loanDelinquencyActions.size());
- Assertions.assertEquals("PAUSE", loanDelinquencyActions.get(0).getAction());
- Assertions.assertEquals(LocalDate.parse("27 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
- Assertions.assertEquals(LocalDate.parse("15 February 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
+ Assertions.assertEquals("PAUSE", loanDelinquencyActions.getFirst().getAction());
+ Assertions.assertEquals(LocalDate.parse("27 January 2023", dateTimeFormatter),
+ loanDelinquencyActions.getFirst().getStartDate());
+ Assertions.assertEquals(LocalDate.parse("15 February 2023", dateTimeFormatter), loanDelinquencyActions.getFirst().getEndDate());
// Loan delinquency data calculation after backdated pause
verifyLoanDelinquencyData(loanId, 3, new InstallmentDelinquencyData(1, 3, BigDecimal.valueOf(250.0)));
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
index 0ae33fa693..8ae4bcc1df 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
@@ -137,8 +137,8 @@ public class LoanProductTestBuilder {
private String minimumGuaranteeFromOwnFunds = null;
private String minimumGuaranteeFromGuarantor = null;
private String isArrearsBasedOnOriginalSchedule = null;
- private String graceOnPrincipalPayment = "1";
- private String graceOnInterestPayment = "1";
+ private String graceOnPrincipalPayment = null;
+ private String graceOnInterestPayment = null;
private JsonObject allowAttributeOverrides = null;
private Boolean allowPartialPeriodInterestCalcualtion = false;
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
index 7848cf6adf..4b7f3f92c3 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
@@ -1368,8 +1368,8 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments()
.withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
- .withExpectedDisbursementDate(date).withSubmittedOnDate(date).withCollaterals(collaterals)
- .build(clientID, loanProductID, null);
+ .withExpectedDisbursementDate(date).withSubmittedOnDate(date).withCollaterals(collaterals).withInArrearsTolerance("0")
+ .withPrincipalGrace("0").withInterestGrace("0").build(clientID, loanProductID, null);
return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON);
}
(fineract) 02/02: FINERACT-2389: Fix the handling of nullable field overrides - E2E tests
Posted by ad...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 8a8675977377aca38ce5264f1ee475ca53c0796b
Author: Peter Kovacs <pe...@dpc.hu>
AuthorDate: Tue Oct 28 10:25:06 2025 +0100
FINERACT-2389: Fix the handling of nullable field overrides - E2E tests
---
.../fineract/test/stepdef/loan/LoanStepDef.java | 80 ++++++++++++++++++++++
.../resources/features/LoanDelinquency.feature | 32 +++++++++
.../resources/features/LoanOverrideFileds.feature | 4 ++
3 files changed, 116 insertions(+)
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 83d589b1a4..b4664fe721 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
@@ -714,6 +714,12 @@ public class LoanStepDef extends AbstractStepDef {
createFullyCustomizedLoanWithInterestRateFrequency(data.get(1));
}
+ @When("Admin creates a fully customized loan with graceOnArrearsAgeing and following data:")
+ public void createFullyCustomizedLoanWithGraceOnArrearsAgeing(final DataTable table) throws IOException {
+ final List<List<String>> data = table.asLists();
+ createFullyCustomizedLoanWithGraceOnArrearsAgeing(data.get(1));
+ }
+
@When("Admin creates a fully customized loan with charges and following data:")
public void createFullyCustomizedLoanWithLoanCharges(final DataTable table) throws IOException {
final List<List<String>> data = table.asLists();
@@ -3758,6 +3764,80 @@ public class LoanStepDef extends AbstractStepDef {
eventCheckHelper.createLoanEventCheck(response);
}
+ public void createFullyCustomizedLoanWithGraceOnArrearsAgeing(final List<String> loanData) 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 interestTypeStr = loanData.get(4);
+ final String interestCalculationPeriodStr = loanData.get(5);
+ final String amortizationTypeStr = 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 repaymentFrequencyTypeStr = 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 String graceOnArrearsAgeingStr = loanData.get(16);
+
+ 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 repaymentFrequencyType = RepaymentFrequencyType.valueOf(repaymentFrequencyTypeStr);
+ final Integer repaymentFrequencyTypeValue = repaymentFrequencyType.getValue();
+
+ final InterestType interestType = InterestType.valueOf(interestTypeStr);
+ final Integer interestTypeValue = interestType.getValue();
+
+ final InterestCalculationPeriodTime interestCalculationPeriod = InterestCalculationPeriodTime.valueOf(interestCalculationPeriodStr);
+ final Integer interestCalculationPeriodValue = interestCalculationPeriod.getValue();
+
+ final AmortizationType amortizationType = AmortizationType.valueOf(amortizationTypeStr);
+ final Integer amortizationTypeValue = amortizationType.getValue();
+
+ final TransactionProcessingStrategyCode processingStrategyCode = TransactionProcessingStrategyCode
+ .valueOf(transactionProcessingStrategyCode);
+ final String transactionProcessingStrategyCodeValue = processingStrategyCode.getValue();
+
+ Integer graceOnArrearsAgeingValue = Integer.valueOf(graceOnArrearsAgeingStr);
+
+ 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)//
+ .graceOnArrearsAgeing(graceOnArrearsAgeingValue);//
+
+ final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
+ testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
+ ErrorHelper.checkSuccessfulApiCall(response);
+
+ eventCheckHelper.createLoanEventCheck(response);
+ }
+
public void createFullyCustomizedLoanWithCharges(final List<String> loanData) throws IOException {
final String loanProduct = loanData.get(0);
final String submitDate = loanData.get(1);
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature
index a382d7e919..e94a899490 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature
@@ -2144,3 +2144,35 @@ Feature: LoanDelinquency
| 3 | RANGE_30 | 125.00 |
| 4 | RANGE_60 | 375.00 |
| 5 | RANGE_90 | 250.00 |
+
+ @TestRailId:C4140
+ Scenario: Verify that loan delinquent days are correct when graceOnArrearsAgeing is set on loan product level (value=3)
+ When Admin sets the business date to "01 January 2025"
+ And Admin creates a client with random data
+ And Admin creates a fully customized loan with 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 |
+ | LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES | 01 January 2025 | 1000 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
+ And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
+ And Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
+ And Admin sets the business date to "15 May 2025"
+ And Admin runs inline COB job for Loan
+ Then Admin checks that delinquency range is: "RANGE_90" and has delinquentDate "2025-02-04"
+ And Loan has the following LOAN level delinquency data:
+ | classification | delinquentAmount | delinquentDate | delinquentDays | pastDueDays |
+ | RANGE_90 | 666.68 | 04 February 2025 | 100 | 103 |
+
+ @TestRailId:C4141
+ Scenario: Verify that loan delinquent days are correct when graceOnArrearsAgeing is overrided on loan level (value=5)
+ When Admin sets the business date to "01 January 2025"
+ And Admin creates a client with random data
+ And Admin creates a fully customized loan with graceOnArrearsAgeing and following data:
+ | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | graceOnArrearsAgeing |
+ | LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES | 01 January 2025 | 1000 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | 5 |
+ And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
+ And Admin successfully disburse the loan on "01 January 2025" with "1000" EUR transaction amount
+ And Admin sets the business date to "15 May 2025"
+ And Admin runs inline COB job for Loan
+ Then Admin checks that delinquency range is: "RANGE_90" and has delinquentDate "2025-02-06"
+ And Loan has the following LOAN level delinquency data:
+ | classification | delinquentAmount | delinquentDate | delinquentDays | pastDueDays |
+ | RANGE_90 | 666.68 | 06 February 2025 | 98 | 103 |
diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature
index b955d0f272..7d8fc9aa93 100644
--- a/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature
+++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanOverrideFileds.feature
@@ -1,6 +1,7 @@
@LoanOverrideFields
Feature: LoanOverrideFields
+ @TestRailId:C4142
Scenario: Verify that all nullable fields default to product when overrides not allowed and not provided
When Admin sets the business date to the actual date
When Admin creates a client with random data
@@ -15,6 +16,7 @@ Feature: LoanOverrideFields
Then LoanDetails has "graceOnInterestPayment" field with value: "1"
Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
+ @TestRailId:C4143
Scenario: Verify that all nullable fields ignore overrides when overrides not allowed
When Admin sets the business date to the actual date
When Admin creates a client with random data
@@ -29,6 +31,7 @@ Feature: LoanOverrideFields
Then LoanDetails has "graceOnInterestPayment" field with value: "1"
Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
+ @TestRailId:C4144
Scenario: Verify that nullable fields default to product when override is allowed but not provided
When Admin sets the business date to the actual date
When Admin creates a client with random data
@@ -43,6 +46,7 @@ Feature: LoanOverrideFields
Then LoanDetails has "graceOnInterestPayment" field with value: "1"
Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
+ @TestRailId:C4145
Scenario: Verify that nullable fields default to product when override is allowed and provided
When Admin sets the business date to the actual date
When Admin creates a client with random data