Skip to content

Commit

Permalink
FINERACT-1806: Advanced Charge-off Expense Accounting - "Advanced Acc…
Browse files Browse the repository at this point in the history
…ounting Rule" takes priority
  • Loading branch information
kulminsky authored and adamsaghy committed Dec 4, 2024
1 parent 5c08afe commit 56ee2ea
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,7 @@ List<ProductToGLAccountMapping> findAllPenaltyToIncomeAccountMappings(@Param("pr
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReasonId is not NULL")
List<ProductToGLAccountMapping> findAllChargesOffReasonsMappings(@Param("productId") Long productId,
@Param("productType") int productType);

@Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReasonId =:chargeOffReasonId")
ProductToGLAccountMapping findChargesOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ public class LoanDTO {
private boolean markedAsChargeOff;
@Setter
private boolean markedAsFraud;
private Integer chargeOffReasonCodeValue;
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +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");

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

return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud);
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
chargeOffReasonCodeValue);
}

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

public SavingsDTO populateSavingsDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
Expand Down Expand Up @@ -227,14 +228,26 @@ private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDT
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final boolean isReversal = loanTransactionDTO.isReversed();
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
// principal payment
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
if (isMarkedFraud) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), glAccountBalanceHolder);

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

ProductToGLAccountMapping mapping = helper.getChargeOffMappingByCodeValue(chargeOffReasonCodeValue);
if (mapping != null) {
GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
glAccountBalanceHolder.addToCredit(accountCredit, principalAmount);
glAccountBalanceHolder.addToDebit(mapping.getGlAccount(), principalAmount);
} else {
// principal payment
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
if (isMarkedFraud) {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), glAccountBalanceHolder);
} else {
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), glAccountBalanceHolder);
}
}
}
// interest payment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* 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.accounting.journalentry;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Collections;
import java.util.List;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.accounting.journalentry.service.AccrualBasedAccountingProcessorForLoan;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class CreateJournalEntriesForChargeOffLoanTest {

private static final Integer chargeOffReasons = 15;

@Mock
private AccountingProcessorHelper helper;
@InjectMocks
private AccrualBasedAccountingProcessorForLoan processor;
private LoanDTO loanDTO;

@BeforeEach
void setUp() {
Office office = Office.headOffice("Main Office", LocalDate.now(ZoneId.systemDefault()), null);
when(helper.getOfficeById(1L)).thenReturn(office);

GLClosure mockClosure = mock(GLClosure.class);
when(helper.getLatestClosureByBranch(1L)).thenReturn(mockClosure);

LoanTransactionEnumData transactionType = mock(LoanTransactionEnumData.class);
when(transactionType.isChargeoff()).thenReturn(true);

LoanTransactionDTO loanTransactionDTO = new LoanTransactionDTO(1L, 1L, "txn-123", LocalDate.now(ZoneId.systemDefault()),
transactionType, new BigDecimal("500.00"), new BigDecimal("500.00"), null, null, null, null, false, Collections.emptyList(),
Collections.emptyList(), false, "", null, null, null, null);

loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasons);
}

@Test
void shouldCreateJournalEntriesForChargeOff() {
GLAccount chargeOffGLAccount = new GLAccount();
chargeOffGLAccount.setId(15L);
chargeOffGLAccount.setName("Charge-Off Account");
chargeOffGLAccount.setGlCode("12345");

ProductToGLAccountMapping chargeToGLAccountMapper = new ProductToGLAccountMapping();
chargeToGLAccountMapper.setGlAccount(chargeOffGLAccount);

when(helper.getChargeOffMappingByCodeValue(chargeOffReasons)).thenReturn(chargeToGLAccountMapper);

GLAccount loanPortfolioGLAccount = new GLAccount();
loanPortfolioGLAccount.setId(20L);
loanPortfolioGLAccount.setName("Loan Portfolio Account");
loanPortfolioGLAccount.setGlCode("54321");

when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L))
.thenReturn(loanPortfolioGLAccount);

processor.createJournalEntriesForLoan(loanDTO);

verify(helper, times(1)).getChargeOffMappingByCodeValue(chargeOffReasons);
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L);
verify(helper, times(1)).createCreditJournalEntryOrReversalForLoan(helper.getOfficeById(1L), "USD",
AccrualAccountsForLoan.LOAN_PORTFOLIO, 1L, null, 1L, "txn-123", LocalDate.now(ZoneId.systemDefault()),
new BigDecimal("500.00"), false);
verify(helper, times(1)).createDebitJournalEntryOrReversalForLoan(helper.getOfficeById(1L), "USD",
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), 1L, null, 1L, "txn-123", LocalDate.now(ZoneId.systemDefault()),
new BigDecimal("500.00"), false);
}

@Test
void shouldCreateJournalEntriesForChargeOffWithFraud() {
loanDTO.setMarkedAsFraud(true);

when(helper.getChargeOffMappingByCodeValue(chargeOffReasons)).thenReturn(null);

GLAccount loanPortfolioGLAccount = new GLAccount();
loanPortfolioGLAccount.setId(20L);
loanPortfolioGLAccount.setName("Loan Portfolio Account");
loanPortfolioGLAccount.setGlCode("54321");

GLAccount fraudExpenseGLAccount = new GLAccount();
fraudExpenseGLAccount.setId(30L);
fraudExpenseGLAccount.setName("Fraud Expense Account");
fraudExpenseGLAccount.setGlCode("98765");

when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L))
.thenReturn(loanPortfolioGLAccount);

when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), 1L))
.thenReturn(fraudExpenseGLAccount);

processor.createJournalEntriesForLoan(loanDTO);

verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L);
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), 1L);
}

@Test
void shouldCreateJournalEntriesForChargeOffWithoutFraud() {
loanDTO.setMarkedAsFraud(false);

when(helper.getChargeOffMappingByCodeValue(chargeOffReasons)).thenReturn(null);

GLAccount loanPortfolioGLAccount = new GLAccount();
loanPortfolioGLAccount.setId(20L);
loanPortfolioGLAccount.setName("Loan Portfolio Account");
loanPortfolioGLAccount.setGlCode("54321");

GLAccount expenseGLAccount = new GLAccount();
expenseGLAccount.setId(40L);
expenseGLAccount.setName("Expense Account");
expenseGLAccount.setGlCode("67890");

when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L))
.thenReturn(loanPortfolioGLAccount);

when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), 1L))
.thenReturn(expenseGLAccount);

processor.createJournalEntriesForLoan(loanDTO);

verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L);
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), 1L);
}
}

0 comments on commit 56ee2ea

Please sign in to comment.