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 fa953838f7f..acd32ad33a6 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 @@ List findAllPenaltyToIncomeAccountMappings(@Param("pr @Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReasonId is not NULL") List 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 60b50dc806c..9ab14adc8fd 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 chargeOffReasonCode; } 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 18502e67c4e..e516dd32e35 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 LoanDTO populateLoanDtoFromMap(final Map accountingBridge boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer"); boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff"); boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud"); + final Integer chargeOffReasonCode = (Integer) accountingBridgeData.get("chargeOffReasonCode"); @SuppressWarnings("unchecked") final List> newTransactionsMap = (List>) accountingBridgeData.get("newLoanTransactions"); @@ -172,7 +173,12 @@ public LoanDTO populateLoanDtoFromMap(final Map accountingBridge } return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled, - periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud); + periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud, + chargeOffReasonCode); + } + + public ProductToGLAccountMapping getChargeOffMappingByCode(Integer chargeOffReasonCode) { + return accountMappingRepository.findChargesOffReasonMappingById(chargeOffReasonCode); } public SavingsDTO populateSavingsDtoFromMap(final Map 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 b9209d3ca32..c1665da0aac 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.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.springframework.stereotype.Component; @@ -229,14 +230,27 @@ 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 chargeOffReasonCode = loanDTO.getChargeOffReasonCode(); + + ProductToGLAccountMapping mapping = helper.getChargeOffMappingByCode(chargeOffReasonCode); + if (mapping != null) { + GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); + glAccountBalanceHolder.addToCredit(accountCredit, principalAmount); + // Resolve Debit + 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 00000000000..8b424b831b0 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java @@ -0,0 +1,162 @@ +/** + * 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.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().minusYears(5), 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(), 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.getChargeOffMappingByCode(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)).getChargeOffMappingByCode(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(), 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(), new BigDecimal("500.00"), + false); + } + + @Test + void shouldCreateJournalEntriesForChargeOffWithFraud() { + loanDTO.setMarkedAsFraud(true); + + when(helper.getChargeOffMappingByCode(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.getChargeOffMappingByCode(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); + } +}