FINERACT-2358: Allow to configure advanced accounting rules based on write-off reason
This commit is contained in:
+98
@@ -85,6 +85,7 @@ import org.apache.fineract.client.models.PostLoansResponse;
|
||||
import org.apache.fineract.client.models.PostRolesRequest;
|
||||
import org.apache.fineract.client.models.PostUsersRequest;
|
||||
import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||
import org.apache.fineract.client.models.PutLoansApprovedAmountRequest;
|
||||
import org.apache.fineract.client.models.PutLoansApprovedAmountResponse;
|
||||
import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountRequest;
|
||||
@@ -507,6 +508,103 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
.loanScheduleType(LoanScheduleType.CUMULATIVE.toString());//
|
||||
}
|
||||
|
||||
protected PutLoanProductsProductIdRequest update4IProgressive(String name, String shortName, Long delinquencyBucketId) {
|
||||
return new PutLoanProductsProductIdRequest().name(name).shortName(shortName).description("4 installment product - progressive")//
|
||||
.includeInBorrowerCycle(false)//
|
||||
.useBorrowerCycle(false)//
|
||||
.currencyCode("EUR")//
|
||||
.digitsAfterDecimal(2)//
|
||||
.principal(1000.0)//
|
||||
.minPrincipal(100.0)//
|
||||
.maxPrincipal(10000.0)//
|
||||
.numberOfRepayments(4)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L.intValue())//
|
||||
.interestRatePerPeriod(10D)//
|
||||
.minInterestRatePerPeriod(0D)//
|
||||
.maxInterestRatePerPeriod(120D)//
|
||||
.interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
|
||||
.isLinkedToFloatingInterestRates(false)//
|
||||
.isLinkedToFloatingInterestRates(false)//
|
||||
.allowVariableInstallments(false)//
|
||||
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
|
||||
.interestType(InterestType.DECLINING_BALANCE)//
|
||||
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
|
||||
.paymentAllocation(List.of(createDefaultPaymentAllocation("NEXT_INSTALLMENT")))//
|
||||
.creditAllocation(List.of())//
|
||||
.overdueDaysForNPA(179)//
|
||||
.daysInMonthType(30L)//
|
||||
.daysInYearType(360L)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.interestRecalculationCompoundingMethod(0)//
|
||||
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.isArrearsBasedOnOriginalSchedule(false)//
|
||||
.isCompoundingToBePostedAsTransaction(false)//
|
||||
.preClosureInterestCalculationStrategy(1)//
|
||||
.allowCompoundingOnEod(false)//
|
||||
.canDefineInstallmentAmount(true)//
|
||||
.repaymentStartDateType(1)//
|
||||
.charges(List.of())//
|
||||
.principalVariationsForBorrowerCycle(List.of())//
|
||||
.interestRateVariationsForBorrowerCycle(List.of())//
|
||||
.numberOfRepaymentVariationsForBorrowerCycle(List.of())//
|
||||
.accountingRule(3)//
|
||||
.canUseForTopup(false)//
|
||||
.fundSourceAccountId(fundSource.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())//
|
||||
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
|
||||
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
|
||||
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
|
||||
.dateFormat(DATETIME_PATTERN)//
|
||||
.locale("en")//
|
||||
.enableAccrualActivityPosting(false)//
|
||||
.multiDisburseLoan(true)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0)//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType("percentage")//
|
||||
.overAppliedNumber(50)//
|
||||
.principalThresholdForLastInstallment(50)//
|
||||
.holdGuaranteeFunds(false)//
|
||||
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||
.amortizationType(true)//
|
||||
.interestType(true)//
|
||||
.transactionProcessingStrategyCode(true)//
|
||||
.interestCalculationPeriodType(true)//
|
||||
.inArrearsTolerance(true)//
|
||||
.repaymentEvery(true)//
|
||||
.graceOnPrincipalAndInterestPayment(true)//
|
||||
.graceOnArrearsAgeing(true)//
|
||||
).isEqualAmortization(false)//
|
||||
.delinquencyBucketId(delinquencyBucketId)//
|
||||
.enableDownPayment(false)//
|
||||
.enableInstallmentLevelDelinquency(false)//
|
||||
.loanScheduleType("PROGRESSIVE")//
|
||||
.loanScheduleProcessingType("HORIZONTAL");
|
||||
}
|
||||
|
||||
protected PostLoanProductsRequest create4IProgressive() {
|
||||
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||
Assertions.assertNotNull(delinquencyBucketId);
|
||||
|
||||
+125
@@ -19,13 +19,23 @@
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoanProductsTemplateResponse;
|
||||
import org.apache.fineract.client.models.GetLoanProductsWriteOffReasonOptions;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.client.models.PostCodeValueDataResponse;
|
||||
import org.apache.fineract.client.models.PostCodeValuesDataRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostWriteOffReasonToExpenseAccountMappings;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.FineractClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeCalculationType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeIncomeType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
|
||||
@@ -36,6 +46,7 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class LoanProductTest extends BaseLoanIntegrationTest {
|
||||
|
||||
@Nested
|
||||
@@ -458,4 +469,118 @@ public class LoanProductTest extends BaseLoanIntegrationTest {
|
||||
.buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue())));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
public class WriteOffReasonsToExpenseMappings {
|
||||
|
||||
@Test
|
||||
public void testWriteOffReasonToExpenseAccountMapping_shouldFail_on_nonExistingGLAccount_And_nonExistingWriteOffReason() {
|
||||
try {
|
||||
loanProductHelper.createLoanProduct(
|
||||
create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(new PostWriteOffReasonToExpenseAccountMappings()
|
||||
.expenseAccountId("101230023").writeOffReasonCodeValueId("201230023")));
|
||||
Assertions.fail("Should have thrown an IllegalArgumentException");
|
||||
} catch (final RuntimeException ex) {
|
||||
Assertions.assertTrue(
|
||||
ex.getMessage().contains("GL Account with ID 101230023 does not exist or is not an Expense GL account"));
|
||||
Assertions.assertTrue(ex.getMessage().contains("Write-off reason with ID 201230023 does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteOffReasonToExpenseAccountMapping_shouldFail_on_nonExistingGLAccount_And_Invalid_expenseAccountId() {
|
||||
try {
|
||||
loanProductHelper.createLoanProduct(create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(
|
||||
new PostWriteOffReasonToExpenseAccountMappings().expenseAccountId("asdf323").writeOffReasonCodeValueId("111")));
|
||||
Assertions.fail("Should have thrown an IllegalArgumentException");
|
||||
} catch (final RuntimeException ex) {
|
||||
Assertions.assertTrue(ex.getMessage()
|
||||
.contains("validation.msg.loanproduct.writeOffReasonsToExpenseMappings[0].expenseAccountId.not.a.number"));
|
||||
Assertions.assertTrue(
|
||||
ex.getMessage().contains("The parameter `writeOffReasonsToExpenseMappings[0].expenseAccountId` must be a number."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteOffReasonToExpenseAccountMapping_shouldFail_on_nonExistingGLAccount_And_Invalid_writeOffReasonCodeValueId() {
|
||||
try {
|
||||
loanProductHelper.createLoanProduct(create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(
|
||||
new PostWriteOffReasonToExpenseAccountMappings().expenseAccountId("111").writeOffReasonCodeValueId("asdf323")));
|
||||
Assertions.fail("Should have thrown an IllegalArgumentException");
|
||||
} catch (final RuntimeException ex) {
|
||||
log.info("Exception: {}", ex.getMessage());
|
||||
Assertions.assertTrue(ex.getMessage()
|
||||
.contains("validation.msg.loanproduct.writeOffReasonsToExpenseMappings[0].writeOffReasonCodeValueId.not.a.number"));
|
||||
Assertions.assertTrue(ex.getMessage()
|
||||
.contains("The parameter `writeOffReasonsToExpenseMappings[0].writeOffReasonCodeValueId` must be a number."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteOffReasonsToExpenseMappings() {
|
||||
|
||||
// create Write Off reasons
|
||||
Long reasonCode1 = createTestWriteOffReason();
|
||||
Long reasonCode2 = createTestWriteOffReason();
|
||||
|
||||
// check if write Off reasons appears on loan product template
|
||||
GetLoanProductsTemplateResponse loanProductTemplate = loanProductHelper.getLoanProductTemplate(false);
|
||||
List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptions = loanProductTemplate.getWriteOffReasonOptions();
|
||||
Assertions.assertNotNull(writeOffReasonOptions);
|
||||
|
||||
boolean isReasonCode1InTemplate = writeOffReasonOptions.stream().map(GetLoanProductsWriteOffReasonOptions::getId)
|
||||
.anyMatch(id -> Objects.equals(id, reasonCode1));
|
||||
boolean isReasonCode2InTemplate = writeOffReasonOptions.stream().map(GetLoanProductsWriteOffReasonOptions::getId)
|
||||
.anyMatch(id -> Objects.equals(id, reasonCode2));
|
||||
Assertions.assertTrue(isReasonCode1InTemplate);
|
||||
Assertions.assertTrue(isReasonCode2InTemplate);
|
||||
|
||||
// Create Test Loan Product
|
||||
String reasonCodeId = reasonCode1.toString();
|
||||
String expenseAccountId = buyDownExpenseAccount.getAccountID().toString();
|
||||
|
||||
Long loanProductId = loanProductHelper.createLoanProduct(
|
||||
create4IProgressive().addWriteOffReasonsToExpenseMappingsItem(new PostWriteOffReasonToExpenseAccountMappings()
|
||||
.expenseAccountId(expenseAccountId).writeOffReasonCodeValueId(reasonCodeId)))
|
||||
.getResourceId();
|
||||
|
||||
// Verify that get loan product API has the corresponding fields
|
||||
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper.retrieveLoanProductById(loanProductId);
|
||||
List<PostWriteOffReasonToExpenseAccountMappings> writeOffReasonToExpenseAccountMappings = getLoanProductsProductIdResponse
|
||||
.getWriteOffReasonsToExpenseMappings();
|
||||
Assertions.assertNotNull(writeOffReasonToExpenseAccountMappings);
|
||||
Assertions.assertEquals(1, writeOffReasonToExpenseAccountMappings.size());
|
||||
PostWriteOffReasonToExpenseAccountMappings writeOffMapping = writeOffReasonToExpenseAccountMappings.getFirst();
|
||||
Assertions.assertNotNull(writeOffMapping);
|
||||
Assertions.assertEquals(expenseAccountId, writeOffMapping.getExpenseAccountId());
|
||||
Assertions.assertEquals(reasonCodeId, writeOffMapping.getWriteOffReasonCodeValueId());
|
||||
|
||||
List<GetLoanProductsWriteOffReasonOptions> writeOffReasonOptionsResultNonTemplate = getLoanProductsProductIdResponse
|
||||
.getWriteOffReasonOptions();
|
||||
if (writeOffReasonOptionsResultNonTemplate != null && !writeOffReasonOptionsResultNonTemplate.isEmpty()) {
|
||||
Assertions.fail("Write-off reason options with no template setting should be empty");
|
||||
}
|
||||
|
||||
// test Update loan product API - delete writeOffReasonsToExpenseMappings
|
||||
|
||||
GetLoanProductsProductIdResponse getLoanProductsProductId = loanProductHelper.retrieveLoanProductById(loanProductId);
|
||||
|
||||
loanProductHelper.updateLoanProductById(loanProductId,
|
||||
update4IProgressive(getLoanProductsProductId.getName(), getLoanProductsProductId.getShortName(),
|
||||
getLoanProductsProductId.getDelinquencyBucket().getId()).writeOffReasonsToExpenseMappings(List.of()));
|
||||
|
||||
// Verify that get loan product API has the corresponding fields
|
||||
Assertions.assertNull(loanProductHelper.retrieveLoanProductById(loanProductId).getWriteOffReasonsToExpenseMappings());
|
||||
}
|
||||
}
|
||||
|
||||
private Long createTestWriteOffReason() {
|
||||
PostCodeValueDataResponse response = okR(FineractClientHelper.getFineractClient().codeValues.createCodeValue(26L,
|
||||
new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("TestWriteOffReason_1_", 6))
|
||||
.description("Test write off reason value 1").isActive(true).position(0)))
|
||||
.body();
|
||||
Assertions.assertNotNull(response);
|
||||
Assertions.assertNotNull(response.getSubResourceId());
|
||||
return response.getSubResourceId();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -834,7 +834,7 @@ public class LoanProductTestBuilder {
|
||||
}
|
||||
Map<String, Long> newMap = new HashMap<>();
|
||||
newMap.put("chargeOffReasonCodeValueId", reasonId);
|
||||
newMap.put("expenseGLAccountId", accountId);
|
||||
newMap.put("expenseAccountId", accountId);
|
||||
this.chargeOffReasonToExpenseAccountMappings.add(newMap);
|
||||
return this;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user