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);
+    }
+}

Reply via email to