FINERACT-1806: Advanced Charge-off Expense Accounting - "Advanced Accounting Rule" takes priority
This commit is contained in:
committed by
Adam Saghy
parent
5c08afec5f
commit
56ee2eaaff
+3
@@ -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")
|
@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,
|
List<ProductToGLAccountMapping> findAllChargesOffReasonsMappings(@Param("productId") Long productId,
|
||||||
@Param("productType") int productType);
|
@Param("productType") int productType);
|
||||||
|
|
||||||
|
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReasonId =:chargeOffReasonId")
|
||||||
|
ProductToGLAccountMapping findChargesOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -45,4 +45,5 @@ public class LoanDTO {
|
|||||||
private boolean markedAsChargeOff;
|
private boolean markedAsChargeOff;
|
||||||
@Setter
|
@Setter
|
||||||
private boolean markedAsFraud;
|
private boolean markedAsFraud;
|
||||||
|
private Integer chargeOffReasonCodeValue;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -112,6 +112,7 @@ public class AccountingProcessorHelper {
|
|||||||
boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer");
|
boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer");
|
||||||
boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff");
|
boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff");
|
||||||
boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud");
|
boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud");
|
||||||
|
final Integer chargeOffReasonCodeValue = (Integer) accountingBridgeData.get("chargeOffReasonCodeValue");
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
final List<Map<String, Object>> newTransactionsMap = (List<Map<String, Object>>) accountingBridgeData.get("newLoanTransactions");
|
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,
|
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,
|
public SavingsDTO populateSavingsDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
|
||||||
|
|||||||
+21
-8
@@ -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.GLAccountBalanceHolder;
|
||||||
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
|
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
|
||||||
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
|
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.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
||||||
@@ -227,14 +228,26 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||||
final boolean isReversal = loanTransactionDTO.isReversed();
|
final boolean isReversal = loanTransactionDTO.isReversed();
|
||||||
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
|
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
|
||||||
// principal payment
|
|
||||||
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
|
// need to fetch if there are account mappings (always one)
|
||||||
if (isMarkedFraud) {
|
Integer chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();
|
||||||
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
|
|
||||||
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), glAccountBalanceHolder);
|
ProductToGLAccountMapping mapping = helper.getChargeOffMappingByCodeValue(chargeOffReasonCodeValue);
|
||||||
} else {
|
if (mapping != null) {
|
||||||
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
|
GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||||
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), glAccountBalanceHolder);
|
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
|
// interest payment
|
||||||
|
|||||||
+164
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user