Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FINERACT-1806: Advanced accounting - Charge-off reason #4241

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ List<ProductToGLAccountMapping> findAllChargeOffReasonsMappings(@Param("productI
@Param("productType") int productType);

@Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReason.id =:chargeOffReasonId")
ProductToGLAccountMapping findChargeOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId);
ProductToGLAccountMapping findChargeOffReasonMappingById(@Param("chargeOffReasonId") Long chargeOffReasonId);

@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId AND mapping.productType =:productType AND mapping.charge IS NULL AND mapping.paymentType IS NULL AND mapping.chargeOffReason IS NULL")
List<ProductToGLAccountMapping> findAllRegularMappings(@Param("productId") Long productId, @Param("productType") Integer productType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2190,13 +2190,9 @@ private Map<String, Object> buildAccountingMapForChargeOffDateCriteria(final Str
boolean isBeforeChargeOffDate) {
final Map<String, Object> accountingBridgeDataChargeOff = new LinkedHashMap<>(
getAccountingBridgeDataGenericAttributes(currencyCode, isAccountTransfer));
if (isBeforeChargeOffDate) {
accountingBridgeDataChargeOff.put("isChargeOff", false);
accountingBridgeDataChargeOff.put("isFraud", isFraud());
} else {
accountingBridgeDataChargeOff.put("isChargeOff", isChargedOff());
accountingBridgeDataChargeOff.put("isFraud", isFraud());
}
accountingBridgeDataChargeOff.put("isChargeOff", !isBeforeChargeOffDate && isChargedOff());
accountingBridgeDataChargeOff.put("isFraud", isFraud());
accountingBridgeDataChargeOff.put("chargeOffReasonCodeValue", fetchChargeOffReasonId());
return accountingBridgeDataChargeOff;
}

Expand Down Expand Up @@ -2274,6 +2270,8 @@ public Map<String, Object> deriveAccountingBridgeData(final String currencyCode,
accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", isPeriodicAccrualAccountingEnabledOnLoanProduct());
accountingBridgeData.put("isAccountTransfer", isAccountTransfer);
accountingBridgeData.put("isChargeOff", isChargedOff());
accountingBridgeData.put("chargeOffReasonCodeValue", fetchChargeOffReasonId());

accountingBridgeData.put("isFraud", isFraud());

final List<Map<String, Object>> newLoanTransactions = new ArrayList<>();
Expand All @@ -2290,6 +2288,10 @@ public Map<String, Object> deriveAccountingBridgeData(final String currencyCode,
return accountingBridgeData;
}

private Long fetchChargeOffReasonId() {
return isChargedOff() && getChargeOffReason() != null ? getChargeOffReason().getId() : null;
}

public Money getReceivableInterest(final LocalDate tillDate) {
Money receivableInterest = Money.zero(getCurrency());
for (final LoanTransaction transaction : this.loanTransactions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ public class LoanDTO {
private boolean markedAsChargeOff;
@Setter
private boolean markedAsFraud;
private Integer chargeOffReasonCodeValue;
private Long chargeOffReasonCodeValue;
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public LoanDTO populateLoanDtoFromMap(final Map<String, Object> accountingBridge
boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer");
boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff");
boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud");
final Integer chargeOffReasonCodeValue = (Integer) accountingBridgeData.get("chargeOffReasonCodeValue");
final Long chargeOffReasonCodeValue = (Long) accountingBridgeData.get("chargeOffReasonCodeValue");

@SuppressWarnings("unchecked")
final List<Map<String, Object>> newTransactionsMap = (List<Map<String, Object>>) accountingBridgeData.get("newLoanTransactions");
Expand Down Expand Up @@ -177,7 +177,7 @@ public LoanDTO populateLoanDtoFromMap(final Map<String, Object> accountingBridge
chargeOffReasonCodeValue);
}

public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Integer chargeOffReasonCodeValue) {
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long chargeOffReasonCodeValue) {
return accountMappingRepository.findChargeOffReasonMappingById(chargeOffReasonCodeValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDT
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();

// need to fetch if there are account mappings (always one)
Integer chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();
Long chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();

ProductToGLAccountMapping mapping = chargeOffReasonCodeValue != null
? helper.getChargeOffMappingByCodeValue(chargeOffReasonCodeValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ private PostCodeValueDataResponse() {

@Schema(example = "4")
public Long resourceId;
@Schema(example = "4")
public Long subResourceId;
}

@Schema(description = "PutCodeValuesDataRequest")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
@ExtendWith(MockitoExtension.class)
class CreateJournalEntriesForChargeOffLoanTest {

private static final Integer chargeOffReasons = 15;
private static final Long chargeOffReasons = 15L;

@Mock
private AccountingProcessorHelper helper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@
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;
Expand All @@ -54,6 +63,7 @@
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;

Expand Down Expand Up @@ -789,6 +799,125 @@ public void noIncomeRecognitionAfterChargeOff() {
}
}

@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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@
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";
Expand Down Expand Up @@ -280,4 +280,11 @@ public static Object updateCodeValue(final RequestSpecification requestSpec, fin
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());
}
}
Loading