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
commit b9564f806beedc714cc45245da1f0a19897ba2bc Author: mariiaKraievska <[email protected]> AuthorDate: Thu Aug 28 14:10:59 2025 +0300 FINERACT-2356: Fix incorrect accounting during write-off in case the loan was already charged-off --- .../apache/fineract/test/data/TransactionType.java | 1 + .../test/resources/features/LoanWriteOff.feature | 64 +++- .../AccrualBasedAccountingProcessorForLoan.java | 326 +++++++++++++++------ 3 files changed, 303 insertions(+), 88 deletions(-) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java index 8029dd6d74..d2ba126997 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java @@ -42,6 +42,7 @@ public enum TransactionType { BUY_DOWN_FEE_ADJUSTMENT("buyDownFeeAdjustment"), // BUY_DOWN_FEE_AMORTIZATION("buyDownFeeAmortization"), // INTEREST_REFUND("interestRefund"), // + WRITE_OFF("writeOff"), // ; public final String value; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature index cbaebe4936..d24f5925e1 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanWriteOff.feature @@ -60,8 +60,64 @@ | Close (as written-off) | 650.0 | 650.0 | 0.0 | 0.0 | 0.0 | 0.0 | Then Admin fails to undo "1"th transaction made on "29 January 2023" + @TestRailId:C4006 + Scenario: Verify accounting during Write-off when loan was already charged-off + When Admin sets the business date to "1 January 2023" + And Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP1_INTEREST_FLAT | 1 January 2023 | 1000 | 12 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER | + And Admin successfully approves the loan on "1 January 2023" with "1000" amount and expected disbursement date on "1 January 2023" + And Admin successfully disburse the loan on "1 January 2023" with "1000" EUR transaction amount + And Admin adds an NSF fee because of payment bounce with "1 January 2023" transaction date + When Admin sets the business date to "22 February 2023" + And Admin adds a 10 % Processing charge to the loan with "en" locale on date: "22 February 2023" + And Admin does charge-off the loan on "22 February 2023" + Then Loan marked as charged-off on "22 February 2023" + Then Loan status will be "ACTIVE" + Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2023" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | 1000.0 | | + | LIABILITY | 145023 | Suspense/Clearing account | | 1000.0 | + Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "22 February 2023" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 143.0 | + | EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0 | | + | INCOME | 404001 | Interest Income Charge Off | 30.0 | | + | INCOME | 404008 | Fee Charge Off | 113.0 | | + When Admin sets the business date to "1 March 2023" + And Admin does write-off the loan on "1 March 2023" + Then Loan status will be "CLOSED_WRITTEN_OFF" + Then Loan Transactions tab has a "WRITE_OFF" transaction with date "01 March 2023" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | EXPENSE | 744007 | Credit Loss/Bad Debt | | 1000.0 | + | INCOME | 404001 | Interest Income Charge Off | | 30.0 | + | INCOME | 404008 | Fee Charge Off | | 113.0 | + | EXPENSE | e4 | Written off | 1143.0 | | - - - - + @TestRailId:C4007 + Scenario: Verify accounting during Write-off when loan was not charged-off before + When Admin sets the business date to "1 January 2023" + And Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP1_INTEREST_FLAT | 1 January 2023 | 1000 | 12 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | PENALTIES_FEES_INTEREST_PRINCIPAL_ORDER | + And Admin successfully approves the loan on "1 January 2023" with "1000" amount and expected disbursement date on "1 January 2023" + And Admin successfully disburse the loan on "1 January 2023" with "1000" EUR transaction amount + And Admin adds an NSF fee because of payment bounce with "1 January 2023" transaction date + When Admin sets the business date to "22 February 2023" + And Admin adds a 10 % Processing charge to the loan with "en" locale on date: "22 February 2023" + Then Loan status will be "ACTIVE" + Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2023" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | 1000.0 | | + | LIABILITY | 145023 | Suspense/Clearing account | | 1000.0 | + When Admin sets the business date to "1 March 2023" + And Admin does write-off the loan on "1 March 2023" + Then Loan status will be "CLOSED_WRITTEN_OFF" + Then Loan Transactions tab has a "WRITE_OFF" transaction with date "01 March 2023" which has the following Journal entries: + | Type | Account code | Account name | Debit | Credit | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 143.0 | + | EXPENSE | e4 | Written off | 1143.0 | | 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 c518f00a60..40ebb2214f 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 @@ -79,8 +79,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess */ else if ((transactionType.isRepaymentType() && !transactionType.isChargeAdjustment()) || transactionType.isRepaymentAtDisbursement() || transactionType.isChargePayment()) { - createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false, - transactionType.isRepaymentAtDisbursement()); + createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office, transactionType.isRepaymentAtDisbursement()); } // Logic for handling recovery payments @@ -100,7 +99,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess // Handle Write Offs else if ((transactionType.isWriteOff() || transactionType.isWaiveInterest() || transactionType.isWaiveCharges())) { - createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false); + createJournalEntriesForWriteOffs(loanDTO, loanTransactionDTO, office); } // Logic for Refunds of Active Loans @@ -1168,35 +1167,32 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess * * <b>Penalty Repayment</b>: Debits "Fund Source" and Credits "Receivable Penalties" <br/> * <br/> - * Handles write offs using the following posting rules <br/> - * <br/> - * <b>Principal Write off</b>: Debits "Losses Written Off" and Credits "Loan Portfolio"<br/> - * - * <b>Interest Write off</b>:Debits "Losses Written off" and Credits "Receivable Interest" <br/> - * - * <b>Fee Write off</b>:Debits "Losses Written off" and Credits "Receivable Fees" <br/> - * - * <b>Penalty Write off</b>: Debits "Losses Written off" and Credits "Receivable Penalties" <br/> - * <br/> - * <br/> * * @param loanTransactionDTO * @param loanDTO * @param office */ - private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, - final Office office, final boolean writeOff, final boolean isIncomeFromFee) { + private void createJournalEntriesForRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office, + final boolean isIncomeFromFee) { final boolean isMarkedChargeOff = loanDTO.isMarkedAsChargeOff(); if (isMarkedChargeOff) { - createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee); + createJournalEntriesForRepaymentWhenLoanIsChargedOff(loanDTO, loanTransactionDTO, office, isIncomeFromFee); + } else { + createJournalEntriesForLoanRepayments(loanDTO, loanTransactionDTO, office, isIncomeFromFee); + } + } + private void createJournalEntriesForWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) { + final boolean isMarkedChargeOff = loanDTO.isMarkedAsChargeOff(); + if (isMarkedChargeOff) { + createJournalEntriesForWriteOffsWhenLoanIsChargedOff(loanDTO, loanTransactionDTO, office); } else { - createJournalEntriesForLoansRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee); + createJournalEntriesForLoanWriteOffs(loanDTO, loanTransactionDTO, office); } } - private void createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, - Office office, boolean writeOff, boolean isIncomeFromFee) { + private void createJournalEntriesForRepaymentWhenLoanIsChargedOff(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, + final Office office, final boolean isIncomeFromFee) { // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); @@ -1212,7 +1208,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); - GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); + final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); BigDecimal totalDebitAmount = new BigDecimal(0); @@ -1240,17 +1236,14 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } - } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), glAccountBalanceHolder); - } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); - } else { populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); @@ -1314,7 +1307,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess this.helper.createCreditJournalEntryForLoanCharges(office, currencyCode, AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount, loanTransactionDTO.getFeePayments()); - GLAccount debitAccount = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + final GLAccount debitAccount = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.FUND_SOURCE.getValue(), paymentTypeId); glAccountBalanceHolder.addToDebit(debitAccount, feesAmount); @@ -1322,7 +1315,6 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); } - } } @@ -1389,30 +1381,25 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess // create credit entries for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) { if (MathUtil.isGreaterThanZero(creditEntry.getValue())) { - GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey()); + final GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey()); this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, creditEntry.getValue(), glAccount); } } if (MathUtil.isGreaterThanZero(totalDebitAmount)) { - if (writeOff) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(), + if (loanTransactionDTO.isLoanToLoanTransfer()) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); + } else if (loanTransactionDTO.isAccountTransfer()) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); } else { - if (loanTransactionDTO.isLoanToLoanTransfer()) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); - } else if (loanTransactionDTO.isAccountTransfer()) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); - } else { - // create debit entries - for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) { - GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey()); - this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, - debitEntry.getValue(), glAccount); - } + // create debit entries + for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) { + final GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey()); + this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, + debitEntry.getValue(), glAccount); } } } @@ -1423,7 +1410,8 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess ***/ if (MathUtil.isGreaterThanZero(totalDebitAmount)) { if (loanTransactionDTO.getTransactionType().isChargeRefund()) { - Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType()); + final Integer incomeAccount = this.helper + .getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType()); this.helper.createJournalEntriesForLoan(office, currencyCode, incomeAccount, AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); } @@ -1431,12 +1419,13 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } - private void createJournalEntriesForLoansRepaymentAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, - final Office office, final boolean writeOff, final boolean isIncomeFromFee) { + private void createJournalEntriesForWriteOffsWhenLoanIsChargedOff(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, + final Office office) { // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); final String currencyCode = loanDTO.getCurrencyCode(); + final boolean isMarkedFraud = loanDTO.isMarkedAsFraud(); // transaction properties final String transactionId = loanTransactionDTO.getTransactionId(); @@ -1447,16 +1436,94 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); BigDecimal totalDebitAmount = new BigDecimal(0); - Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>(); - Map<Integer, BigDecimal> debitAccountMapForGoodwillCredit = new LinkedHashMap<>(); + // principal payment + if (MathUtil.isGreaterThanZero(principalAmount)) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + glAccountBalanceHolder); + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); + } + } - // handle principal payment or writeOff + // interest payment + if (MathUtil.isGreaterThanZero(interestAmount)) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + glAccountBalanceHolder); + } + + // handle fees payment + if (MathUtil.isGreaterThanZero(feesAmount)) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); + } + + // handle penalties + if (MathUtil.isGreaterThanZero(penaltiesAmount)) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + glAccountBalanceHolder); + } + + // overpayment + if (MathUtil.isGreaterThanZero(overPaymentAmount)) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), glAccountBalanceHolder); + } + + // create credit entries + for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) { + if (MathUtil.isGreaterThanZero(creditEntry.getValue())) { + final GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey()); + this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, + creditEntry.getValue(), glAccount); + } + } + + if (MathUtil.isGreaterThanZero(totalDebitAmount)) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); + } + } + + private void createJournalEntriesForLoanRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, + final Office office, final boolean isIncomeFromFee) { + // loan properties + final Long loanProductId = loanDTO.getLoanProductId(); + final Long loanId = loanDTO.getLoanId(); + final String currencyCode = loanDTO.getCurrencyCode(); + + // transaction properties + final String transactionId = loanTransactionDTO.getTransactionId(); + final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final BigDecimal principalAmount = loanTransactionDTO.getPrincipal(); + final BigDecimal interestAmount = loanTransactionDTO.getInterest(); + final BigDecimal feesAmount = loanTransactionDTO.getFees(); + final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); + final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + final Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>(); + final Map<Integer, BigDecimal> debitAccountMapForGoodwillCredit = new LinkedHashMap<>(); + + // handle principal payment if (MathUtil.isGreaterThanZero(principalAmount)) { totalDebitAmount = totalDebitAmount.add(principalAmount); - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); accountMap.put(account, principalAmount); if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { @@ -1465,13 +1532,13 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } } - // handle interest payment of writeOff + // handle interest payment if (MathUtil.isGreaterThanZero(interestAmount)) { totalDebitAmount = totalDebitAmount.add(interestAmount); - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId); if (accountMap.containsKey(account)) { - BigDecimal amount = accountMap.get(account).add(interestAmount); + final BigDecimal amount = accountMap.get(account).add(interestAmount); accountMap.put(account, amount); } else { accountMap.put(account, interestAmount); @@ -1483,17 +1550,17 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } } - // handle fees payment of writeOff + // handle fees payment if (MathUtil.isGreaterThanZero(feesAmount)) { totalDebitAmount = totalDebitAmount.add(feesAmount); if (isIncomeFromFee) { this.helper.createCreditJournalEntryForLoanCharges(office, currencyCode, AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount, loanTransactionDTO.getFeePayments()); } else { - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId); if (accountMap.containsKey(account)) { - BigDecimal amount = accountMap.get(account).add(feesAmount); + final BigDecimal amount = accountMap.get(account).add(feesAmount); accountMap.put(account, amount); } else { accountMap.put(account, feesAmount); @@ -1505,23 +1572,23 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } } - // handle penalties payment of writeOff + // handle penalties payment if (MathUtil.isGreaterThanZero(penaltiesAmount)) { totalDebitAmount = totalDebitAmount.add(penaltiesAmount); if (isIncomeFromFee) { - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), paymentTypeId); if (accountMap.containsKey(account)) { - BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + final BigDecimal amount = accountMap.get(account).add(penaltiesAmount); accountMap.put(account, amount); } else { accountMap.put(account, penaltiesAmount); } } else { - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId); if (accountMap.containsKey(account)) { - BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + final BigDecimal amount = accountMap.get(account).add(penaltiesAmount); accountMap.put(account, amount); } else { accountMap.put(account, penaltiesAmount); @@ -1537,10 +1604,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess if (MathUtil.isGreaterThanZero(overPaymentAmount)) { totalDebitAmount = totalDebitAmount.add(overPaymentAmount); - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), - paymentTypeId); + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId); if (accountMap.containsKey(account)) { - BigDecimal amount = accountMap.get(account).add(overPaymentAmount); + final BigDecimal amount = accountMap.get(account).add(overPaymentAmount); accountMap.put(account, amount); } else { accountMap.put(account, overPaymentAmount); @@ -1559,31 +1626,26 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } /** - * Single DEBIT transaction for write-offs or Repayments + * Single DEBIT transaction for Repayments ***/ if (MathUtil.isGreaterThanZero(totalDebitAmount)) { - if (writeOff) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(), + if (loanTransactionDTO.isLoanToLoanTransfer()) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); + } else if (loanTransactionDTO.isAccountTransfer()) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); } else { - if (loanTransactionDTO.isLoanToLoanTransfer()) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); - } else if (loanTransactionDTO.isAccountTransfer()) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); - } else { - if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { - // create debit entries - for (Map.Entry<Integer, BigDecimal> debitEntry : debitAccountMapForGoodwillCredit.entrySet()) { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, - paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue()); - } - - } else { - this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.FUND_SOURCE.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + // create debit entries + for (Map.Entry<Integer, BigDecimal> debitEntry : debitAccountMapForGoodwillCredit.entrySet()) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue()); } + + } else { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.FUND_SOURCE.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); } } } @@ -1593,12 +1655,108 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess * repayment above ***/ if (MathUtil.isGreaterThanZero(totalDebitAmount) && loanTransactionDTO.getTransactionType().isChargeRefund()) { - Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType()); + final Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType()); this.helper.createJournalEntriesForLoan(office, currencyCode, incomeAccount, AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); } } + private void createJournalEntriesForLoanWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, + final Office office) { + // loan properties + final Long loanProductId = loanDTO.getLoanProductId(); + final Long loanId = loanDTO.getLoanId(); + final String currencyCode = loanDTO.getCurrencyCode(); + + // transaction properties + final String transactionId = loanTransactionDTO.getTransactionId(); + final LocalDate transactionDate = loanTransactionDTO.getTransactionDate(); + final BigDecimal principalAmount = loanTransactionDTO.getPrincipal(); + final BigDecimal interestAmount = loanTransactionDTO.getInterest(); + final BigDecimal feesAmount = loanTransactionDTO.getFees(); + final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties(); + final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment(); + final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + final Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>(); + + // handle principal payment of writeOff + if (MathUtil.isGreaterThanZero(principalAmount)) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); + accountMap.put(account, principalAmount); + } + + // handle interest payment of writeOff + if (MathUtil.isGreaterThanZero(interestAmount)) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + final BigDecimal amount = accountMap.get(account).add(interestAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, interestAmount); + } + } + + // handle fees payment of writeOff + if (MathUtil.isGreaterThanZero(feesAmount)) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + final BigDecimal amount = accountMap.get(account).add(feesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, feesAmount); + } + } + + // handle penalties payment of writeOff + if (MathUtil.isGreaterThanZero(penaltiesAmount)) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + final BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, penaltiesAmount); + } + } + + if (MathUtil.isGreaterThanZero(overPaymentAmount)) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + final GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + final BigDecimal amount = accountMap.get(account).add(overPaymentAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, overPaymentAmount); + } + } + + for (Map.Entry<GLAccount, BigDecimal> entry : accountMap.entrySet()) { + if (MathUtil.isGreaterThanZero(entry.getValue())) { + this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate, entry.getValue(), + entry.getKey()); + } + } + + /** + * Single DEBIT transaction for write-offs + ***/ + if (MathUtil.isGreaterThanZero(totalDebitAmount)) { + this.helper.createDebitJournalEntryForLoan(office, currencyCode, AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount); + } + } + private void populateDebitAccountEntry(Long loanProductId, BigDecimal transactionPartAmount, Integer debitAccountType, Map<Integer, BigDecimal> accountMapForDebit, Long paymentTypeId) { Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType,
