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 8029dd6d7..d2ba12699 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 cbaebe493..d24f5925e 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 c518f00a6..40ebb2214 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 * * Penalty Repayment: Debits "Fund Source" and Credits "Receivable Penalties"
*
- * Handles write offs using the following posting rules
- *
- * Principal Write off: Debits "Losses Written Off" and Credits "Loan Portfolio"
- * - * Interest Write off:Debits "Losses Written off" and Credits "Receivable Interest"
- * - * Fee Write off:Debits "Losses Written off" and Credits "Receivable Fees"
- * - * Penalty Write off: Debits "Losses Written off" and Credits "Receivable Penalties"
- *
- *
* * @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 { - createJournalEntriesForLoansRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee); + createJournalEntriesForLoanRepayments(loanDTO, loanTransactionDTO, office, isIncomeFromFee); } } - private void createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, - Office office, boolean writeOff, boolean 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 { + createJournalEntriesForLoanWriteOffs(loanDTO, loanTransactionDTO, office); + } + } + + 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 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 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 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,8 +1419,87 @@ 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(); + 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(); + final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + // 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); + } + } + + // 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 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(); @@ -1450,13 +1517,13 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess BigDecimal totalDebitAmount = new BigDecimal(0); - Map accountMap = new LinkedHashMap<>(); - Map debitAccountMapForGoodwillCredit = new LinkedHashMap<>(); + final Map accountMap = new LinkedHashMap<>(); + final Map debitAccountMapForGoodwillCredit = new LinkedHashMap<>(); - // handle principal payment or writeOff + // 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 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 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 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 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 accountMapForDebit, Long paymentTypeId) { Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType,