Skip to content

Commit 56ee2ea

Browse files
kulminskyadamsaghy
authored andcommitted
FINERACT-1806: Advanced Charge-off Expense Accounting - "Advanced Accounting Rule" takes priority
1 parent 5c08afe commit 56ee2ea

File tree

5 files changed

+196
-9
lines changed

5 files changed

+196
-9
lines changed

fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/domain/ProductToGLAccountMappingRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,7 @@ List<ProductToGLAccountMapping> findAllPenaltyToIncomeAccountMappings(@Param("pr
6565
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.productId =:productId and mapping.productType =:productType and mapping.chargeOffReasonId is not NULL")
6666
List<ProductToGLAccountMapping> findAllChargesOffReasonsMappings(@Param("productId") Long productId,
6767
@Param("productType") int productType);
68+
69+
@Query("select mapping from ProductToGLAccountMapping mapping where mapping.chargeOffReasonId =:chargeOffReasonId")
70+
ProductToGLAccountMapping findChargesOffReasonMappingById(@Param("chargeOffReasonId") Integer chargeOffReasonId);
6871
}

fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ public class LoanDTO {
4545
private boolean markedAsChargeOff;
4646
@Setter
4747
private boolean markedAsFraud;
48+
private Integer chargeOffReasonCodeValue;
4849
}

fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public LoanDTO populateLoanDtoFromMap(final Map<String, Object> accountingBridge
112112
boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer");
113113
boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff");
114114
boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud");
115+
final Integer chargeOffReasonCodeValue = (Integer) accountingBridgeData.get("chargeOffReasonCodeValue");
115116

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

174175
return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
175-
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud);
176+
periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
177+
chargeOffReasonCodeValue);
178+
}
179+
180+
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Integer chargeOffReasonCodeValue) {
181+
return accountMappingRepository.findChargesOffReasonMappingById(chargeOffReasonCodeValue);
176182
}
177183

178184
public SavingsDTO populateSavingsDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,

fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
3535
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
3636
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
37+
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
3738
import org.apache.fineract.infrastructure.core.service.MathUtil;
3839
import org.apache.fineract.organisation.office.domain.Office;
3940
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
@@ -227,14 +228,26 @@ private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDT
227228
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
228229
final boolean isReversal = loanTransactionDTO.isReversed();
229230
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
230-
// principal payment
231-
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
232-
if (isMarkedFraud) {
233-
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
234-
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), glAccountBalanceHolder);
235-
} else {
236-
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
237-
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), glAccountBalanceHolder);
231+
232+
// need to fetch if there are account mappings (always one)
233+
Integer chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();
234+
235+
ProductToGLAccountMapping mapping = helper.getChargeOffMappingByCodeValue(chargeOffReasonCodeValue);
236+
if (mapping != null) {
237+
GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
238+
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
239+
glAccountBalanceHolder.addToCredit(accountCredit, principalAmount);
240+
glAccountBalanceHolder.addToDebit(mapping.getGlAccount(), principalAmount);
241+
} else {
242+
// principal payment
243+
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
244+
if (isMarkedFraud) {
245+
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
246+
AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), glAccountBalanceHolder);
247+
} else {
248+
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
249+
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), glAccountBalanceHolder);
250+
}
238251
}
239252
}
240253
// interest payment
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.accounting.journalentry;
20+
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.times;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
26+
import java.math.BigDecimal;
27+
import java.time.LocalDate;
28+
import java.time.ZoneId;
29+
import java.util.Collections;
30+
import java.util.List;
31+
import org.apache.fineract.accounting.closure.domain.GLClosure;
32+
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
33+
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
34+
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
35+
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
36+
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
37+
import org.apache.fineract.accounting.journalentry.service.AccrualBasedAccountingProcessorForLoan;
38+
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
39+
import org.apache.fineract.organisation.office.domain.Office;
40+
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
41+
import org.junit.jupiter.api.BeforeEach;
42+
import org.junit.jupiter.api.Test;
43+
import org.junit.jupiter.api.extension.ExtendWith;
44+
import org.mockito.InjectMocks;
45+
import org.mockito.Mock;
46+
import org.mockito.junit.jupiter.MockitoExtension;
47+
48+
@ExtendWith(MockitoExtension.class)
49+
class CreateJournalEntriesForChargeOffLoanTest {
50+
51+
private static final Integer chargeOffReasons = 15;
52+
53+
@Mock
54+
private AccountingProcessorHelper helper;
55+
@InjectMocks
56+
private AccrualBasedAccountingProcessorForLoan processor;
57+
private LoanDTO loanDTO;
58+
59+
@BeforeEach
60+
void setUp() {
61+
Office office = Office.headOffice("Main Office", LocalDate.now(ZoneId.systemDefault()), null);
62+
when(helper.getOfficeById(1L)).thenReturn(office);
63+
64+
GLClosure mockClosure = mock(GLClosure.class);
65+
when(helper.getLatestClosureByBranch(1L)).thenReturn(mockClosure);
66+
67+
LoanTransactionEnumData transactionType = mock(LoanTransactionEnumData.class);
68+
when(transactionType.isChargeoff()).thenReturn(true);
69+
70+
LoanTransactionDTO loanTransactionDTO = new LoanTransactionDTO(1L, 1L, "txn-123", LocalDate.now(ZoneId.systemDefault()),
71+
transactionType, new BigDecimal("500.00"), new BigDecimal("500.00"), null, null, null, null, false, Collections.emptyList(),
72+
Collections.emptyList(), false, "", null, null, null, null);
73+
74+
loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true, List.of(loanTransactionDTO), false, false, chargeOffReasons);
75+
}
76+
77+
@Test
78+
void shouldCreateJournalEntriesForChargeOff() {
79+
GLAccount chargeOffGLAccount = new GLAccount();
80+
chargeOffGLAccount.setId(15L);
81+
chargeOffGLAccount.setName("Charge-Off Account");
82+
chargeOffGLAccount.setGlCode("12345");
83+
84+
ProductToGLAccountMapping chargeToGLAccountMapper = new ProductToGLAccountMapping();
85+
chargeToGLAccountMapper.setGlAccount(chargeOffGLAccount);
86+
87+
when(helper.getChargeOffMappingByCodeValue(chargeOffReasons)).thenReturn(chargeToGLAccountMapper);
88+
89+
GLAccount loanPortfolioGLAccount = new GLAccount();
90+
loanPortfolioGLAccount.setId(20L);
91+
loanPortfolioGLAccount.setName("Loan Portfolio Account");
92+
loanPortfolioGLAccount.setGlCode("54321");
93+
94+
when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L))
95+
.thenReturn(loanPortfolioGLAccount);
96+
97+
processor.createJournalEntriesForLoan(loanDTO);
98+
99+
verify(helper, times(1)).getChargeOffMappingByCodeValue(chargeOffReasons);
100+
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L);
101+
verify(helper, times(1)).createCreditJournalEntryOrReversalForLoan(helper.getOfficeById(1L), "USD",
102+
AccrualAccountsForLoan.LOAN_PORTFOLIO, 1L, null, 1L, "txn-123", LocalDate.now(ZoneId.systemDefault()),
103+
new BigDecimal("500.00"), false);
104+
verify(helper, times(1)).createDebitJournalEntryOrReversalForLoan(helper.getOfficeById(1L), "USD",
105+
AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), 1L, null, 1L, "txn-123", LocalDate.now(ZoneId.systemDefault()),
106+
new BigDecimal("500.00"), false);
107+
}
108+
109+
@Test
110+
void shouldCreateJournalEntriesForChargeOffWithFraud() {
111+
loanDTO.setMarkedAsFraud(true);
112+
113+
when(helper.getChargeOffMappingByCodeValue(chargeOffReasons)).thenReturn(null);
114+
115+
GLAccount loanPortfolioGLAccount = new GLAccount();
116+
loanPortfolioGLAccount.setId(20L);
117+
loanPortfolioGLAccount.setName("Loan Portfolio Account");
118+
loanPortfolioGLAccount.setGlCode("54321");
119+
120+
GLAccount fraudExpenseGLAccount = new GLAccount();
121+
fraudExpenseGLAccount.setId(30L);
122+
fraudExpenseGLAccount.setName("Fraud Expense Account");
123+
fraudExpenseGLAccount.setGlCode("98765");
124+
125+
when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L))
126+
.thenReturn(loanPortfolioGLAccount);
127+
128+
when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), 1L))
129+
.thenReturn(fraudExpenseGLAccount);
130+
131+
processor.createJournalEntriesForLoan(loanDTO);
132+
133+
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L);
134+
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), 1L);
135+
}
136+
137+
@Test
138+
void shouldCreateJournalEntriesForChargeOffWithoutFraud() {
139+
loanDTO.setMarkedAsFraud(false);
140+
141+
when(helper.getChargeOffMappingByCodeValue(chargeOffReasons)).thenReturn(null);
142+
143+
GLAccount loanPortfolioGLAccount = new GLAccount();
144+
loanPortfolioGLAccount.setId(20L);
145+
loanPortfolioGLAccount.setName("Loan Portfolio Account");
146+
loanPortfolioGLAccount.setGlCode("54321");
147+
148+
GLAccount expenseGLAccount = new GLAccount();
149+
expenseGLAccount.setId(40L);
150+
expenseGLAccount.setName("Expense Account");
151+
expenseGLAccount.setGlCode("67890");
152+
153+
when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L))
154+
.thenReturn(loanPortfolioGLAccount);
155+
156+
when(helper.getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), 1L))
157+
.thenReturn(expenseGLAccount);
158+
159+
processor.createJournalEntriesForLoan(loanDTO);
160+
161+
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), 1L);
162+
verify(helper, times(1)).getLinkedGLAccountForLoanProduct(1L, AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), 1L);
163+
}
164+
}

0 commit comments

Comments
 (0)