FINERACT-1806: Advanced accounting - Charge-off reason

This commit is contained in:
Adam Saghy
2025-01-07 19:40:45 +01:00
parent b7e6f356da
commit e1d043bb2d
10 changed files with 159 additions and 18 deletions
@@ -159,6 +159,7 @@ public abstract class BaseLoanIntegrationTest {
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
protected GlobalConfigurationHelper globalConfigurationHelper = new GlobalConfigurationHelper();
protected final CodeHelper codeHelper = new CodeHelper();
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
@@ -26,12 +26,21 @@ import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.apache.fineract.client.models.AllowAttributeOverrides;
import org.apache.fineract.client.models.GetCodesResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostChargeOffReasonToExpenseAccountMappings;
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.PostLoansLoanIdChargesChargeIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
@@ -54,6 +63,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -789,6 +799,125 @@ public class LoanChargeOffAccountingTest extends BaseLoanIntegrationTest {
}
}
@Test
public void advancedAccountingForChargeOff() {
runAt("02 January 2023", () -> {
final Account chargeOffDelinquentExpenseAccount = accountHelper
.createExpenseAccount("delinquent_expense_for_charge_off_reason");
GetCodesResponse chargeOffReasonCode = fetchChargeOffReasonCode();
PostCodeValueDataResponse chargeOffReason = codeHelper.createCodeValue(chargeOffReasonCode.getId(),
new PostCodeValuesDataRequest().name(Utils.uniqueRandomStringGenerator("DELINQUENT_", 6)).isActive(true).position(10));
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsResponse productsResponse = createLoanProductWithAdvancedChargeOffAccounting(chargeOffReason,
chargeOffDelinquentExpenseAccount);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, productsResponse.getResourceId(), "01 January 2023", 1000.0, 1);
// Disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
PostLoansLoanIdTransactionsResponse chargeOffTransaction = this.loanTransactionHelper.chargeOffLoan(loanId,
new PostLoansLoanIdTransactionsRequest().transactionDate("02 January 2023").locale("en").dateFormat("dd MMMM yyyy")
.chargeOffReasonId(chargeOffReason.getSubResourceId()));
// verify journal entries
verifyTRJournalEntries(chargeOffTransaction.getResourceId(), journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
journalEntry(1000.0, chargeOffDelinquentExpenseAccount, "DEBIT"));
});
}
private PostLoanProductsResponse createLoanProductWithAdvancedChargeOffAccounting(PostCodeValueDataResponse chargeOffReason,
Account chargeOffDelinquentExpenseAccount) {
return this.loanTransactionHelper.createLoanProduct(new PostLoanProductsRequest()
.name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
.description("Loan Product Description")//
.includeInBorrowerCycle(false)//
.currencyCode("USD")//
.digitsAfterDecimal(2)//
.inMultiplesOf(0)//
.installmentAmountInMultiplesOf(1)//
.useBorrowerCycle(false)//
.minPrincipal(100.0)//
.principal(1000.0)//
.maxPrincipal(100000.0)//
.minNumberOfRepayments(1)//
.numberOfRepayments(1)//
.maxNumberOfRepayments(30)//
.isLinkedToFloatingInterestRates(false)//
.minInterestRatePerPeriod((double) 0)//
.interestRatePerPeriod(0.0)//
.maxInterestRatePerPeriod((double) 100)//
.interestRateFrequencyType(2)//
.repaymentEvery(30)//
.repaymentFrequencyType(0L)//
.amortizationType(1)//
.interestType(0)//
.isEqualAmortization(false)//
.interestCalculationPeriodType(1)//
.transactionProcessingStrategyCode(
LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)//
.loanScheduleType(LoanScheduleType.CUMULATIVE.toString()) //
.daysInYearType(1)//
.daysInMonthType(1)//
.canDefineInstallmentAmount(true)//
.graceOnArrearsAgeing(3)//
.overdueDaysForNPA(179)//
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
.principalThresholdForLastInstallment(50)//
.allowVariableInstallments(false)//
.canUseForTopup(false)//
.isInterestRecalculationEnabled(false)//
.holdGuaranteeFunds(false)//
.multiDisburseLoan(true)//
.allowAttributeOverrides(new AllowAttributeOverrides()//
.amortizationType(true)//
.interestType(true)//
.transactionProcessingStrategyCode(true)//
.interestCalculationPeriodType(true)//
.inArrearsTolerance(true)//
.repaymentEvery(true)//
.graceOnPrincipalAndInterestPayment(true)//
.graceOnArrearsAgeing(true))//
.allowPartialPeriodInterestCalcualtion(true)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0)//
.charges(Collections.emptyList())//
.accountingRule(3)//
.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())//
.addChargeOffReasonToExpenseAccountMappingsItem(
new PostChargeOffReasonToExpenseAccountMappings().chargeOffReasonCodeValueId(chargeOffReason.getSubResourceId())
.expenseAccountId(chargeOffDelinquentExpenseAccount.getAccountID().longValue()))
.dateFormat(DATETIME_PATTERN)//
.locale("en_GB")//
.disallowExpectedDisbursements(true)//
.allowApprovedDisbursedAmountsOverApplied(true)//
.overAppliedCalculationType("percentage")//
.overAppliedNumber(50));
}
private GetCodesResponse fetchChargeOffReasonCode() {
return codeHelper.retrieveCodes().stream().filter(c -> "ChargeOffReasons".equals(c.getName())).findFirst().orElseThrow();
}
private Integer createLoanAccount(final Integer clientID, final Integer loanProductID, final String externalId) {
String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1")
@@ -27,19 +27,19 @@ import io.restassured.specification.ResponseSpecification;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.apache.fineract.client.models.GetCodesResponse;
import org.apache.fineract.client.models.PostCodeValueDataResponse;
import org.apache.fineract.client.models.PostCodeValuesDataRequest;
import org.apache.fineract.integrationtests.client.IntegrationTest;
import org.apache.fineract.integrationtests.common.Utils;
public final class CodeHelper {
public final class CodeHelper extends IntegrationTest {
private static final String COUNTRY_CODE_NAME = "COUNTRY";
private static final String STATE_CODE_NAME = "STATE";
private static final String ADDRESS_TYPE_CODE_NAME = "ADDRESS_TYPE";
private static final String CHARGE_OFF_REASONS_CODE_NAME = "ChargeOffReasons";
private CodeHelper() {
}
public static final String CODE_ID_ATTRIBUTE_NAME = "id";
public static final String RESPONSE_ID_ATTRIBUTE_NAME = "resourceId";
public static final String SUBRESPONSE_ID_ATTRIBUTE_NAME = "subResourceId";
@@ -280,4 +280,11 @@ public final class CodeHelper {
getTestCodeValueAsJSON(codeValueName, description, position), jsonAttributeToGetback);
}
public PostCodeValueDataResponse createCodeValue(Long codeId, PostCodeValuesDataRequest request) {
return ok(fineract().codeValues.createCodeValue(codeId, request));
}
public List<GetCodesResponse> retrieveCodes() {
return ok(fineract().codes.retrieveCodes());
}
}