FINERACT-2113: Advanced Charge-off Expense Accounting - Add new "Advanced Accounting Rule"
This commit is contained in:
committed by
Adam Saghy
parent
75fa740ff6
commit
4ccb33a95e
+272
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import static org.apache.fineract.integrationtests.common.funds.FundsResourceHandler.createFund;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.AllowAttributeOverrides;
|
||||
import org.apache.fineract.client.models.ChargeData;
|
||||
import org.apache.fineract.client.models.ChargeToGLAccountMapper;
|
||||
import org.apache.fineract.client.models.GetChargeOffReasonsToExpenseMappings;
|
||||
import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings;
|
||||
import org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
||||
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
|
||||
import org.apache.fineract.integrationtests.common.system.CodeHelper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
@ExtendWith(LoanTestLifecycleExtension.class)
|
||||
public class LoanProductChargeOffReasonMappingsTest extends BaseLoanIntegrationTest {
|
||||
|
||||
private static final String CODE_VALUE_NAME = "ChargeOffReasons";
|
||||
|
||||
private static ResponseSpecification responseSpec;
|
||||
private static RequestSpecification requestSpec;
|
||||
private static LoanTransactionHelper loanTransactionHelper;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
Utils.initializeRESTAssured();
|
||||
requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
requestSpec.header("Fineract-Platform-TenantId", "default");
|
||||
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
|
||||
BusinessStepHelper businessStepHelper = new BusinessStepHelper();
|
||||
// setup COB Business Steps to prevent test failing due other integration test configurations
|
||||
businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
|
||||
"CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
|
||||
"EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateLoanProductWithValidChargeOffReason() {
|
||||
final String creationBusinessDay = "15 January 2023";
|
||||
runAt(creationBusinessDay, () -> {
|
||||
Integer chargeOffReasons = createChargeOffReason();
|
||||
Long localLoanProductId = loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 15L))
|
||||
.getResourceId();
|
||||
|
||||
Assertions.assertNotNull(localLoanProductId);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateLoanProductWithValidChargeOffReason() {
|
||||
final String creationBusinessDay = "15 January 2023";
|
||||
runAt(creationBusinessDay, () -> {
|
||||
Integer chargeOffReasons = createChargeOffReason();
|
||||
List<GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings = new ArrayList<>();
|
||||
GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings();
|
||||
getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(Long.valueOf(chargeOffReasons));
|
||||
getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(15L);
|
||||
chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings);
|
||||
|
||||
Long localLoanProductId = loanTransactionHelper.updateLoanProduct(1L,
|
||||
new PutLoanProductsProductIdRequest().locale("en").chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings))
|
||||
.getResourceId();
|
||||
|
||||
Assertions.assertNotNull(localLoanProductId);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateLoanProductWithInvalidGLAccount() {
|
||||
final String creationBusinessDay = "15 January 2023";
|
||||
runAt(creationBusinessDay, () -> {
|
||||
try {
|
||||
Integer chargeOffReasons = createChargeOffReason();
|
||||
loanTransactionHelper.createLoanProduct(loanProductsRequest(Long.valueOf(chargeOffReasons), 9999L));
|
||||
} catch (CallFailedRuntimeException e) {
|
||||
Assertions.assertTrue(e.getMessage().contains("validation.msg.glaccount.not.found"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateLoanProductWithInvalidChargeOffReason() {
|
||||
final String creationBusinessDay = "15 January 2023";
|
||||
runAt(creationBusinessDay, () -> {
|
||||
try {
|
||||
loanTransactionHelper.createLoanProduct(loanProductsRequest(1L, 12L));
|
||||
} catch (CallFailedRuntimeException e) {
|
||||
Assertions.assertTrue(e.getMessage().contains("validation.msg.chargeoffreason.invalid"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private PostLoanProductsRequest loanProductsRequest(Long chargeOffReasonId, Long glAccountId) {
|
||||
String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
|
||||
String shortName = Utils.uniqueRandomStringGenerator("", 4);
|
||||
|
||||
List<Integer> principalVariationsForBorrowerCycle = new ArrayList<>();
|
||||
List<Integer> numberOfRepaymentVariationsForBorrowerCycle = new ArrayList<>();
|
||||
List<Integer> interestRateVariationsForBorrowerCycle = new ArrayList<>();
|
||||
List<ChargeData> charges = new ArrayList<>();
|
||||
List<ChargeToGLAccountMapper> penaltyToIncomeAccountMappings = new ArrayList<>();
|
||||
List<GetLoanFeeToIncomeAccountMappings> feeToIncomeAccountMappings = new ArrayList<>();
|
||||
|
||||
List<GetChargeOffReasonsToExpenseMappings> chargeOffReasonsToExpenseMappings = new ArrayList<>();
|
||||
GetChargeOffReasonsToExpenseMappings getChargeOffReasonsToExpenseMappings = new GetChargeOffReasonsToExpenseMappings();
|
||||
getChargeOffReasonsToExpenseMappings.setChargeOffReasonCodeValueId(chargeOffReasonId);
|
||||
getChargeOffReasonsToExpenseMappings.setExpenseGLAccountId(glAccountId);
|
||||
chargeOffReasonsToExpenseMappings.add(getChargeOffReasonsToExpenseMappings);
|
||||
|
||||
List<GetLoanPaymentChannelToFundSourceMappings> paymentChannelToFundSourceMappings = new ArrayList<>();
|
||||
GetLoanPaymentChannelToFundSourceMappings loanPaymentChannelToFundSourceMappings = new GetLoanPaymentChannelToFundSourceMappings();
|
||||
loanPaymentChannelToFundSourceMappings.fundSourceAccountId(fundSource.getAccountID().longValue());
|
||||
loanPaymentChannelToFundSourceMappings.paymentTypeId(1L);
|
||||
paymentChannelToFundSourceMappings.add(loanPaymentChannelToFundSourceMappings);
|
||||
|
||||
final Integer fundId = createFund(requestSpec, responseSpec);
|
||||
Assertions.assertNotNull(fundId);
|
||||
|
||||
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||
Assertions.assertNotNull(delinquencyBucketId);
|
||||
|
||||
return new PostLoanProductsRequest()//
|
||||
.name(name)//
|
||||
.enableAccrualActivityPosting(true)//
|
||||
.shortName(shortName)//
|
||||
.description(
|
||||
"LP1 with 12% DECLINING BALANCE interest, interest period: Daily, Interest recalculation-Daily, Compounding:none")//
|
||||
.fundId(fundId.longValue())//
|
||||
.startDate(null)//
|
||||
.closeDate(null)//
|
||||
.includeInBorrowerCycle(false)//
|
||||
.currencyCode("EUR")//
|
||||
.digitsAfterDecimal(2)//
|
||||
.inMultiplesOf(1)//
|
||||
.installmentAmountInMultiplesOf(1)//
|
||||
.useBorrowerCycle(false)//
|
||||
.minPrincipal(100.0)//
|
||||
.principal(1000.0)//
|
||||
.maxPrincipal(10000.0)//
|
||||
.minNumberOfRepayments(1)//
|
||||
.numberOfRepayments(1)//
|
||||
.maxNumberOfRepayments(30)//
|
||||
.isLinkedToFloatingInterestRates(false)//
|
||||
.minInterestRatePerPeriod(0.0)//
|
||||
.interestRatePerPeriod(12.0)//
|
||||
.maxInterestRatePerPeriod(30.0)//
|
||||
.interestRateFrequencyType(3)//
|
||||
.repaymentEvery(30)//
|
||||
.repaymentFrequencyType(0L)//
|
||||
.principalVariationsForBorrowerCycle(principalVariationsForBorrowerCycle)//
|
||||
.numberOfRepaymentVariationsForBorrowerCycle(numberOfRepaymentVariationsForBorrowerCycle)//
|
||||
.interestRateVariationsForBorrowerCycle(interestRateVariationsForBorrowerCycle)//
|
||||
.amortizationType(1)//
|
||||
.interestType(0)//
|
||||
.isEqualAmortization(false)//
|
||||
.interestCalculationPeriodType(0)//
|
||||
.transactionProcessingStrategyCode("mifos-standard-strategy")//
|
||||
.daysInYearType(1)//
|
||||
.daysInMonthType(1)//
|
||||
.canDefineInstallmentAmount(true)//
|
||||
.graceOnArrearsAgeing(3)//
|
||||
.overdueDaysForNPA(179)//
|
||||
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||
.principalThresholdForLastInstallment(50)//
|
||||
.allowVariableInstallments(false)//
|
||||
.canUseForTopup(false)//
|
||||
.holdGuaranteeFunds(false)//
|
||||
.multiDisburseLoan(false)//
|
||||
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||
.amortizationType(true)//
|
||||
.interestType(true)//
|
||||
.transactionProcessingStrategyCode(true)//
|
||||
.interestCalculationPeriodType(true)//
|
||||
.inArrearsTolerance(true)//
|
||||
.repaymentEvery(true)//
|
||||
.graceOnPrincipalAndInterestPayment(true)//
|
||||
.graceOnArrearsAgeing(true))
|
||||
.outstandingLoanBalance(10000.0)//
|
||||
.charges(charges)//
|
||||
.accountingRule(3)//
|
||||
|
||||
.fundSourceAccountId(suspenseAccount.getAccountID().longValue())//
|
||||
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
|
||||
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
|
||||
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
|
||||
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
|
||||
.incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue())//
|
||||
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
|
||||
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
|
||||
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
|
||||
.receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())//
|
||||
.receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())//
|
||||
.receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())//
|
||||
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
|
||||
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||
.incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
|
||||
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
|
||||
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
|
||||
|
||||
.dateFormat("dd MMMM yyyy")//
|
||||
.locale("en")//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||
.delinquencyBucketId(delinquencyBucketId.longValue())//
|
||||
.paymentChannelToFundSourceMappings(paymentChannelToFundSourceMappings)//
|
||||
.penaltyToIncomeAccountMappings(penaltyToIncomeAccountMappings)//
|
||||
.chargeOffReasonsToExpenseMappings(chargeOffReasonsToExpenseMappings).feeToIncomeAccountMappings(feeToIncomeAccountMappings)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.preClosureInterestCalculationStrategy(1)//
|
||||
.rescheduleStrategyMethod(3)//
|
||||
.interestRecalculationCompoundingMethod(0)//
|
||||
.recalculationRestFrequencyType(2)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.allowPartialPeriodInterestCalcualtion(false);//
|
||||
}
|
||||
|
||||
private Integer createChargeOffReason() {
|
||||
Integer chargeOffReasonId;
|
||||
HashMap<String, Object> codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME);
|
||||
if (codes.isEmpty()) {
|
||||
CodeHelper.createCode(requestSpec, responseSpec, CODE_VALUE_NAME, "");
|
||||
}
|
||||
codes = CodeHelper.getCodeByName(requestSpec, responseSpec, CODE_VALUE_NAME);
|
||||
Integer codeId = (Integer) codes.get("id");
|
||||
HashMap<String, Object> codeValues = CodeHelper.getOrCreateCodeValueByCodeIdAndCodeName(requestSpec, responseSpec, codeId,
|
||||
CODE_VALUE_NAME, 1);
|
||||
chargeOffReasonId = (Integer) codeValues.get("id");
|
||||
return chargeOffReasonId;
|
||||
}
|
||||
}
|
||||
+16
@@ -107,6 +107,7 @@ public class LoanProductTestBuilder {
|
||||
|
||||
private List<Map<String, Long>> feeToIncomeAccountMappings = null;
|
||||
private List<Map<String, Long>> penaltyToIncomeAccountMappings = null;
|
||||
private List<Map<String, Long>> chargeOffReasonsToExpenseMappings = null;
|
||||
private Account feeAndPenaltyAssetAccount;
|
||||
|
||||
private Boolean multiDisburseLoan = false;
|
||||
@@ -303,6 +304,10 @@ public class LoanProductTestBuilder {
|
||||
map.put("penaltyToIncomeAccountMappings", this.penaltyToIncomeAccountMappings);
|
||||
}
|
||||
|
||||
if (this.chargeOffReasonsToExpenseMappings != null) {
|
||||
map.put("chargeOffReasonsToExpenseMappings", this.chargeOffReasonsToExpenseMappings);
|
||||
}
|
||||
|
||||
if (this.dueDaysForRepaymentEvent != null) {
|
||||
map.put("dueDaysForRepaymentEvent", this.dueDaysForRepaymentEvent);
|
||||
}
|
||||
@@ -796,6 +801,17 @@ public class LoanProductTestBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanProductTestBuilder withChargeOffReasonsToExpenseMappings(final Long reasonId, final Long accountId) {
|
||||
if (this.chargeOffReasonsToExpenseMappings == null) {
|
||||
this.chargeOffReasonsToExpenseMappings = new ArrayList<>();
|
||||
}
|
||||
Map<String, Long> newMap = new HashMap<>();
|
||||
newMap.put("chargeOffReasonCodeValueId", reasonId);
|
||||
newMap.put("expenseGLAccountId", accountId);
|
||||
this.chargeOffReasonsToExpenseMappings.add(newMap);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTransactionProcessingStrategyCode() {
|
||||
return transactionProcessingStrategyCode;
|
||||
}
|
||||
|
||||
+36
@@ -93,6 +93,35 @@ public final class CodeHelper {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static HashMap<String, Object> getOrCreateCodeValueByCodeIdAndCodeName(final RequestSpecification requestSpec,
|
||||
final ResponseSpecification responseSpec, final Integer codeId, final String codeName, final Integer position) {
|
||||
|
||||
ArrayList<HashMap<String, Object>> allCodeValues = CodeHelper.getAllCodeValuesByCodeId(requestSpec, responseSpec, codeId);
|
||||
HashMap<String, Object> codesByName = filterCodesByName(allCodeValues, codeName);
|
||||
|
||||
if (codesByName.isEmpty()) {
|
||||
CodeHelper.createCodeValue(requestSpec, responseSpec, codeId, codeName, position);
|
||||
allCodeValues = CodeHelper.getAllCodeValuesByCodeId(requestSpec, responseSpec, codeId);
|
||||
}
|
||||
|
||||
return filterCodesByName(allCodeValues, codeName);
|
||||
}
|
||||
|
||||
private static HashMap<String, Object> filterCodesByName(ArrayList<HashMap<String, Object>> allCodeValues, String codeName) {
|
||||
final HashMap<String, Object> codes = new HashMap<>();
|
||||
|
||||
for (HashMap<String, Object> map : allCodeValues) {
|
||||
String name = (String) map.get("name");
|
||||
if (name.equals(codeName)) {
|
||||
codes.put("id", map.get("id"));
|
||||
codes.put("name", map.get("name"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
public static HashMap<String, Object> retrieveOrCreateCodeValue(Integer codeId, final RequestSpecification requestSpec,
|
||||
final ResponseSpecification responseSpec) {
|
||||
Integer codeValueId = null;
|
||||
@@ -119,6 +148,13 @@ public final class CodeHelper {
|
||||
|
||||
}
|
||||
|
||||
public static ArrayList<HashMap<String, Object>> getAllCodeValuesByCodeId(final RequestSpecification requestSpec,
|
||||
final ResponseSpecification responseSpec, final Integer codeId) {
|
||||
|
||||
return Utils.performServerGet(requestSpec, responseSpec,
|
||||
CODE_VALUE_URL.replace("[codeId]", codeId.toString()) + "?" + Utils.TENANT_IDENTIFIER, "");
|
||||
}
|
||||
|
||||
public static Object getSystemDefinedCodes(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
|
||||
|
||||
final String getResponse = given().spec(requestSpec).expect().spec(responseSpec).when()
|
||||
|
||||
Reference in New Issue
Block a user