FINERACT-2389: Fix the handling of nullable field overrides

This commit is contained in:
Oleksii Novikov
2025-10-29 11:26:16 +01:00
committed by Adam Saghy
parent b32837e9b4
commit adc4ee3545
11 changed files with 290 additions and 29 deletions
@@ -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
@@ -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,
@@ -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);
}
}
}
@@ -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";
}