This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 56ee2eaaf FINERACT-1806: Advanced Charge-off Expense Accounting -
"Advanced Accounting Rule" takes priority
56ee2eaaf is described below
commit 56ee2eaaffe6ddee443264d69b18f1f2425658f2
Author: Andrii Kulminskyi <[email protected]>
AuthorDate: Tue Nov 26 17:53:12 2024 +0200
FINERACT-1806: Advanced Charge-off Expense Accounting - "Advanced
Accounting Rule" takes priority
---
.../ProductToGLAccountMappingRepository.java | 3 +
.../accounting/journalentry/data/LoanDTO.java | 1 +
.../service/AccountingProcessorHelper.java | 8 +-
.../AccrualBasedAccountingProcessorForLoan.java | 29 +++-
.../CreateJournalEntriesForChargeOffLoanTest.java | 164 +++++++++++++++++++++
5 files changed, 196 insertions(+), 9 deletions(-)
diff --git
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
index fa953838f..acd32ad33 100644
---
a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
+++
b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java
@@ -65,4 +65,7 @@ public interface ProductToGLAccountMappingRepository
@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);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
index 60b50dc80..bbdf9f3c7 100755
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
@@ -45,4 +45,5 @@ public class LoanDTO {
private boolean markedAsChargeOff;
@Setter
private boolean markedAsFraud;
+ private Integer chargeOffReasonCodeValue;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
index 18502e67c..1cac8808f 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
@@ -112,6 +112,7 @@ public class AccountingProcessorHelper {
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");
@@ -172,7 +173,12 @@ public class AccountingProcessorHelper {
}
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,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index 0a79bcacf..eaf22e98d 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -34,6 +34,7 @@ import
org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
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;
@@ -227,14 +228,26 @@ public class AccrualBasedAccountingProcessorForLoan
implements AccountingProcess
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
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
new file mode 100644
index 000000000..07a183f51
--- /dev/null
+++
b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
@@ -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);
+ }
+}