Skip to content

Commit adc4ee3

Browse files
oleksii-novikov-onixadamsaghy
authored andcommitted
FINERACT-2389: Fix the handling of nullable field overrides
1 parent b32837e commit adc4ee3

File tree

11 files changed

+290
-29
lines changed

11 files changed

+290
-29
lines changed

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ public enum DefaultLoanProduct implements LoanProduct {
170170
LP1_INTEREST_FLAT_DAILY_RECALCULATION_DAILY_360_30_MULTIDISB, //
171171
LP1_INTEREST_FLAT_SAR_RECALCULATION_SAME_AS_REPAYMENT_MULTIDISB_AUTO_DOWNPAYMENT, //
172172
LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY, //
173+
LP1_WITH_OVERRIDES, //
174+
LP1_NO_OVERRIDES //
173175
;
174176

175177
@Override

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.concurrent.atomic.AtomicInteger;
3737
import lombok.RequiredArgsConstructor;
3838
import org.apache.fineract.client.models.AdvancedPaymentData;
39+
import org.apache.fineract.client.models.AllowAttributeOverrides;
3940
import org.apache.fineract.client.models.CreditAllocationData;
4041
import org.apache.fineract.client.models.CreditAllocationOrder;
4142
import org.apache.fineract.client.models.LoanProductChargeData;
@@ -4056,6 +4057,54 @@ public void initialize() throws Exception {
40564057
TestContext.INSTANCE.set(
40574058
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY,
40584059
responseLoanProductsResponseAdvCustomPaymentAllocationProgressiveLoanInterestDailyEmi36030InterestRecalculationDaily);
4060+
4061+
// (LP1_WITH_OVERRIDES) - Loan product with all attribute overrides ENABLED
4062+
final String nameWithOverrides = DefaultLoanProduct.LP1_WITH_OVERRIDES.getName();
4063+
final PostLoanProductsRequest loanProductsRequestWithOverrides = loanProductsRequestFactory.defaultLoanProductsRequestLP1() //
4064+
.name(nameWithOverrides) //
4065+
.interestRatePerPeriod(1.0) //
4066+
.maxInterestRatePerPeriod(30.0) //
4067+
.inArrearsTolerance(10) //
4068+
.graceOnPrincipalPayment(1) //
4069+
.graceOnInterestPayment(1) //
4070+
.graceOnArrearsAgeing(3) //
4071+
.numberOfRepayments(6) //
4072+
.allowAttributeOverrides(new AllowAttributeOverrides() //
4073+
.amortizationType(true) //
4074+
.interestType(true) //
4075+
.transactionProcessingStrategyCode(true) //
4076+
.interestCalculationPeriodType(true) //
4077+
.inArrearsTolerance(true) //
4078+
.repaymentEvery(true) //
4079+
.graceOnPrincipalAndInterestPayment(true) //
4080+
.graceOnArrearsAgeing(true));
4081+
final Response<PostLoanProductsResponse> responseWithOverrides = loanProductsApi.createLoanProduct(loanProductsRequestWithOverrides)
4082+
.execute();
4083+
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_WITH_OVERRIDES, responseWithOverrides);
4084+
4085+
// (LP1_NO_OVERRIDES) - Loan product with all attribute overrides DISABLED
4086+
final String nameNoOverrides = DefaultLoanProduct.LP1_NO_OVERRIDES.getName();
4087+
final PostLoanProductsRequest loanProductsRequestNoOverrides = loanProductsRequestFactory.defaultLoanProductsRequestLP1() //
4088+
.name(nameNoOverrides) //
4089+
.interestRatePerPeriod(1.0) //
4090+
.maxInterestRatePerPeriod(30.0) //
4091+
.inArrearsTolerance(10) //
4092+
.graceOnPrincipalPayment(1) //
4093+
.graceOnInterestPayment(1) //
4094+
.graceOnArrearsAgeing(3) //
4095+
.numberOfRepayments(6) //
4096+
.allowAttributeOverrides(new AllowAttributeOverrides() //
4097+
.amortizationType(false) //
4098+
.interestType(false) //
4099+
.transactionProcessingStrategyCode(false) //
4100+
.interestCalculationPeriodType(false) //
4101+
.inArrearsTolerance(false) //
4102+
.repaymentEvery(false) //
4103+
.graceOnPrincipalAndInterestPayment(false) //
4104+
.graceOnArrearsAgeing(false));
4105+
final Response<PostLoanProductsResponse> responseNoOverrides = loanProductsApi.createLoanProduct(loanProductsRequestNoOverrides)
4106+
.execute();
4107+
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_NO_OVERRIDES, responseNoOverrides);
40594108
}
40604109

40614110
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.test.stepdef.loan;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.junit.jupiter.api.Assertions.assertNotNull;
23+
24+
import io.cucumber.datatable.DataTable;
25+
import io.cucumber.java.en.Then;
26+
import io.cucumber.java.en.When;
27+
import java.io.IOException;
28+
import java.math.BigDecimal;
29+
import java.util.Map;
30+
import lombok.RequiredArgsConstructor;
31+
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
32+
import org.apache.fineract.client.models.PostClientsResponse;
33+
import org.apache.fineract.client.models.PostLoansRequest;
34+
import org.apache.fineract.client.models.PostLoansResponse;
35+
import org.apache.fineract.client.services.LoansApi;
36+
import org.apache.fineract.test.data.loanproduct.DefaultLoanProduct;
37+
import org.apache.fineract.test.data.loanproduct.LoanProductResolver;
38+
import org.apache.fineract.test.factory.LoanRequestFactory;
39+
import org.apache.fineract.test.helper.ErrorHelper;
40+
import org.apache.fineract.test.stepdef.AbstractStepDef;
41+
import org.apache.fineract.test.support.TestContextKey;
42+
import retrofit2.Response;
43+
44+
@RequiredArgsConstructor
45+
public class LoanOverrideFieldsStepDef extends AbstractStepDef {
46+
47+
private final LoanRequestFactory loanRequestFactory;
48+
private final LoanProductResolver loanProductResolver;
49+
private final LoansApi loansApi;
50+
51+
@Then("LoanDetails has {string} field with value: {string}")
52+
public void checkLoanDetailsFieldWithValue(final String fieldName, final String expectedValue) throws IOException {
53+
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
54+
assertNotNull(loanResponse.body());
55+
final Long loanId = loanResponse.body().getLoanId();
56+
57+
final Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "", "", "").execute();
58+
ErrorHelper.checkSuccessfulApiCall(loanDetails);
59+
assertNotNull(loanDetails.body());
60+
61+
verifyFieldValue(loanDetails.body(), fieldName, expectedValue);
62+
}
63+
64+
private void verifyFieldValue(final GetLoansLoanIdResponse loanDetails, final String fieldName, final String expectedValue) {
65+
final Integer actualValue = getIntFieldValue(loanDetails, fieldName);
66+
final Integer expected = Integer.valueOf(expectedValue);
67+
assertThat(actualValue).as("Expected %s to be %d but was %s", fieldName, expected, actualValue).isEqualTo(expected);
68+
}
69+
70+
private Integer getIntFieldValue(final GetLoansLoanIdResponse loanDetails, final String fieldName) {
71+
return switch (fieldName) {
72+
case "inArrearsTolerance" -> loanDetails.getInArrearsTolerance();
73+
case "graceOnPrincipalPayment" -> loanDetails.getGraceOnPrincipalPayment();
74+
case "graceOnInterestPayment" -> loanDetails.getGraceOnInterestPayment();
75+
case "graceOnArrearsAgeing" -> loanDetails.getGraceOnArrearsAgeing();
76+
default -> throw new IllegalArgumentException("Unknown override field: " + fieldName);
77+
};
78+
}
79+
80+
@When("Admin creates a new Loan with the following override data:")
81+
public void createLoanWithOverrideData(final DataTable dataTable) throws IOException {
82+
final Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
83+
assertNotNull(clientResponse.body());
84+
final Long clientId = clientResponse.body().getClientId();
85+
86+
final Map<String, String> overrideData = dataTable.asMap(String.class, String.class);
87+
88+
final String loanProductName = overrideData.get("loanProduct");
89+
if (loanProductName == null) {
90+
throw new IllegalArgumentException("loanProduct is required in override data");
91+
}
92+
93+
final PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId)
94+
.productId(loanProductResolver.resolve(DefaultLoanProduct.valueOf(loanProductName))).numberOfRepayments(6)
95+
.loanTermFrequency(180).interestRatePerPeriod(new BigDecimal(1));
96+
97+
overrideData.forEach((fieldName, value) -> {
98+
if (!"loanProduct".equals(fieldName)) {
99+
applyOverrideField(loansRequest, fieldName, value);
100+
}
101+
});
102+
103+
final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
104+
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
105+
ErrorHelper.checkSuccessfulApiCall(response);
106+
}
107+
108+
private void applyOverrideField(final PostLoansRequest request, final String fieldName, final String value) {
109+
final boolean isNull = "null".equals(value);
110+
111+
switch (fieldName) {
112+
case "inArrearsTolerance" -> request.inArrearsTolerance(isNull ? null : new BigDecimal(value));
113+
case "graceOnInterestPayment" -> request.graceOnInterestPayment(isNull ? null : Integer.valueOf(value));
114+
case "graceOnPrincipalPayment" -> request.graceOnPrincipalPayment(isNull ? null : Integer.valueOf(value));
115+
case "graceOnArrearsAgeing" -> request.graceOnArrearsAgeing(isNull ? null : Integer.valueOf(value));
116+
default -> throw new IllegalArgumentException("Unknown override field: " + fieldName);
117+
}
118+
}
119+
120+
}

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,6 @@ public abstract class TestContextKey {
275275
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentInterestRecalculationMultidisbursalApprovedOverAppliedAmountExpectedTransches";
276276
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";
277277
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_WRITE_OFF_REASON_MAP = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentWriteOffReasonMap";
278+
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_WITH_OVERRIDES = "loanProductCreateResponseLP1WithOverrides";
279+
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_NO_OVERRIDES = "loanProductCreateResponseLP1NoOverrides";
278280
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@LoanOverrideFields
2+
Feature: LoanOverrideFields
3+
4+
Scenario: Verify that all nullable fields default to product when overrides not allowed and not provided
5+
When Admin sets the business date to the actual date
6+
When Admin creates a client with random data
7+
When Admin creates a new Loan with the following override data:
8+
| loanProduct | LP1_NO_OVERRIDES |
9+
| inArrearsTolerance | null |
10+
| graceOnPrincipalPayment | null |
11+
| graceOnInterestPayment | null |
12+
| graceOnArrearsAgeing | null |
13+
Then LoanDetails has "inArrearsTolerance" field with value: "10"
14+
Then LoanDetails has "graceOnPrincipalPayment" field with value: "1"
15+
Then LoanDetails has "graceOnInterestPayment" field with value: "1"
16+
Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
17+
18+
Scenario: Verify that all nullable fields ignore overrides when overrides not allowed
19+
When Admin sets the business date to the actual date
20+
When Admin creates a client with random data
21+
When Admin creates a new Loan with the following override data:
22+
| loanProduct | LP1_NO_OVERRIDES |
23+
| inArrearsTolerance | 11 |
24+
| graceOnPrincipalPayment | 2 |
25+
| graceOnInterestPayment | 2 |
26+
| graceOnArrearsAgeing | 4 |
27+
Then LoanDetails has "inArrearsTolerance" field with value: "10"
28+
Then LoanDetails has "graceOnPrincipalPayment" field with value: "1"
29+
Then LoanDetails has "graceOnInterestPayment" field with value: "1"
30+
Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
31+
32+
Scenario: Verify that nullable fields default to product when override is allowed but not provided
33+
When Admin sets the business date to the actual date
34+
When Admin creates a client with random data
35+
When Admin creates a new Loan with the following override data:
36+
| loanProduct | LP1_WITH_OVERRIDES |
37+
| inArrearsTolerance | null |
38+
| graceOnPrincipalPayment | null |
39+
| graceOnInterestPayment | null |
40+
| graceOnArrearsAgeing | null |
41+
Then LoanDetails has "inArrearsTolerance" field with value: "10"
42+
Then LoanDetails has "graceOnPrincipalPayment" field with value: "1"
43+
Then LoanDetails has "graceOnInterestPayment" field with value: "1"
44+
Then LoanDetails has "graceOnArrearsAgeing" field with value: "3"
45+
46+
Scenario: Verify that nullable fields default to product when override is allowed and provided
47+
When Admin sets the business date to the actual date
48+
When Admin creates a client with random data
49+
When Admin creates a new Loan with the following override data:
50+
| loanProduct | LP1_WITH_OVERRIDES |
51+
| inArrearsTolerance | 11 |
52+
| graceOnPrincipalPayment | 2 |
53+
| graceOnInterestPayment | 2 |
54+
| graceOnArrearsAgeing | 4 |
55+
Then LoanDetails has "inArrearsTolerance" field with value: "11"
56+
Then LoanDetails has "graceOnPrincipalPayment" field with value: "2"
57+
Then LoanDetails has "graceOnInterestPayment" field with value: "2"
58+
Then LoanDetails has "graceOnArrearsAgeing" field with value: "4"

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,14 @@ private GetLoansLoanIdLoanTermEnumData() {}
12161216
public Boolean chargedOff;
12171217
@Schema(example = "3")
12181218
public Integer inArrearsTolerance;
1219+
@Schema(example = "0")
1220+
public Integer graceOnPrincipalPayment;
1221+
@Schema(example = "0")
1222+
public Integer graceOnInterestPayment;
1223+
@Schema(example = "0")
1224+
public Integer graceOnInterestCharged;
1225+
@Schema(example = "3")
1226+
public Integer graceOnArrearsAgeing;
12191227
@Schema(example = "false")
12201228
public Boolean enableDownPayment;
12211229
@Schema(example = "0.000000")
@@ -1348,6 +1356,8 @@ private PostLoansRequest() {}
13481356
public Integer graceOnInterestPayment;
13491357
@Schema(example = "1")
13501358
public Integer graceOnArrearsAgeing;
1359+
@Schema(example = "10")
1360+
public BigDecimal inArrearsTolerance;
13511361
@Schema(example = "HORIZONTAL")
13521362
public String loanScheduleProcessingType;
13531363
@Schema(example = "false")

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -350,27 +350,33 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement
350350
}
351351

352352
// grace details
353-
final Integer graceOnPrincipalPayment = allowOverridingGraceOnPrincipalAndInterestPayment
354-
? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element)
355-
: loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment();
353+
Integer graceOnPrincipalPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnPrincipalPayment", element);
354+
if (!allowOverridingGraceOnPrincipalAndInterestPayment || graceOnPrincipalPayment == null) {
355+
graceOnPrincipalPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnPrincipalPayment();
356+
}
356357
final Integer recurringMoratoriumOnPrincipalPeriods = this.fromApiJsonHelper
357358
.extractIntegerWithLocaleNamed("recurringMoratoriumOnPrincipalPeriods", element);
358-
final Integer graceOnInterestPayment = allowOverridingGraceOnPrincipalAndInterestPayment
359-
? this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element)
360-
: loanProduct.getLoanProductRelatedDetail().getGraceOnInterestPayment();
359+
Integer graceOnInterestPayment = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestPayment", element);
360+
if (!allowOverridingGraceOnPrincipalAndInterestPayment || graceOnInterestPayment == null) {
361+
graceOnInterestPayment = loanProduct.getLoanProductRelatedDetail().getGraceOnInterestPayment();
362+
}
361363
final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element);
362364
final LocalDate interestChargedFromDate = this.fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element);
363365
final Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = this.configurationDomainService
364366
.isInterestChargedFromDateSameAsDisbursementDate();
365367

366-
final Integer graceOnArrearsAgeing = allowOverridingGraceOnArrearsAging
367-
? this.fromApiJsonHelper.extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element)
368-
: loanProduct.getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
368+
Integer graceOnArrearsAgeing = this.fromApiJsonHelper
369+
.extractIntegerWithLocaleNamed(LoanProductConstants.GRACE_ON_ARREARS_AGEING_PARAMETER_NAME, element);
370+
if (!allowOverridingGraceOnArrearsAging || graceOnArrearsAgeing == null) {
371+
graceOnArrearsAgeing = loanProduct.getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
372+
}
369373

370374
// other
371-
final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
372-
final Money inArrearsToleranceMoney = allowOverridingArrearsTolerance ? Money.of(currency, inArrearsTolerance)
373-
: loanProduct.getLoanProductRelatedDetail().getInArrearsTolerance();
375+
BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
376+
if (!allowOverridingArrearsTolerance || inArrearsTolerance == null) {
377+
inArrearsTolerance = loanProduct.getLoanProductRelatedDetail().getInArrearsTolerance().getAmount();
378+
}
379+
final Money inArrearsToleranceMoney = Money.of(currency, inArrearsTolerance);
374380

375381
final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName,
376382
element);

0 commit comments

Comments
 (0)