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";
}
@@ -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"
@@ -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")
@@ -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);
@@ -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);
}
@@ -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)));
@@ -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;
@@ -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);
}