diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java index dcc168565..ca0786ad2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/common/AccountingConstants.java @@ -38,8 +38,9 @@ public final class AccountingConstants { FUND_SOURCE(1), LOAN_PORTFOLIO(2), INTEREST_ON_LOANS(3), INCOME_FROM_FEES(4), INCOME_FROM_PENALTIES(5), LOSSES_WRITTEN_OFF( 6), TRANSFERS_SUSPENSE(10), OVERPAYMENT(11), INCOME_FROM_RECOVERY(12), GOODWILL_CREDIT(13), INCOME_FROM_CHARGE_OFF_INTEREST( - 14), INCOME_FROM_CHARGE_OFF_FEES( - 15), CHARGE_OFF_EXPENSE(16), CHARGE_OFF_FRAUD_EXPENSE(17), INCOME_FROM_CHARGE_OFF_PENALTY(18); + 14), INCOME_FROM_CHARGE_OFF_FEES(15), CHARGE_OFF_EXPENSE(16), CHARGE_OFF_FRAUD_EXPENSE( + 17), INCOME_FROM_CHARGE_OFF_PENALTY(18), INCOME_FROM_GOODWILL_CREDIT_INTEREST( + 19), INCOME_FROM_GOODWILL_CREDIT_FEES(20), INCOME_FROM_GOODWILL_CREDIT_PENALTY(21); private final Integer value; @@ -78,8 +79,9 @@ public final class AccountingConstants { FUND_SOURCE(1), LOAN_PORTFOLIO(2), INTEREST_ON_LOANS(3), INCOME_FROM_FEES(4), INCOME_FROM_PENALTIES(5), // LOSSES_WRITTEN_OFF(6), INTEREST_RECEIVABLE(7), FEES_RECEIVABLE(8), PENALTIES_RECEIVABLE(9), // TRANSFERS_SUSPENSE(10), OVERPAYMENT(11), INCOME_FROM_RECOVERY(12), GOODWILL_CREDIT(13), INCOME_FROM_CHARGE_OFF_INTEREST( - 14), INCOME_FROM_CHARGE_OFF_FEES( - 15), CHARGE_OFF_EXPENSE(16), CHARGE_OFF_FRAUD_EXPENSE(17), INCOME_FROM_CHARGE_OFF_PENALTY(18); + 14), INCOME_FROM_CHARGE_OFF_FEES(15), CHARGE_OFF_EXPENSE(16), CHARGE_OFF_FRAUD_EXPENSE(17), INCOME_FROM_CHARGE_OFF_PENALTY( + 18), INCOME_FROM_GOODWILL_CREDIT_INTEREST( + 19), INCOME_FROM_GOODWILL_CREDIT_FEES(20), INCOME_FROM_GOODWILL_CREDIT_PENALTY(21); private final Integer value; @@ -134,7 +136,10 @@ public final class AccountingConstants { "incomeFromChargeOffFeesAccountId"), CHARGE_OFF_EXPENSE( "chargeOffExpenseAccountId"), CHARGE_OFF_FRAUD_EXPENSE( "chargeOffFraudExpenseAccountId"), INCOME_FROM_CHARGE_OFF_PENALTY( - "incomeFromChargeOffPenaltyAccountId"); + "incomeFromChargeOffPenaltyAccountId"), INCOME_FROM_GOODWILL_CREDIT_INTEREST( + "incomeFromGoodwillCreditInterestAccountId"), INCOME_FROM_GOODWILL_CREDIT_FEES( + "incomeFromGoodwillCreditFeesAccountId"), INCOME_FROM_GOODWILL_CREDIT_PENALTY( + "incomeFromGoodwillCreditPenaltyAccountId"); private final String value; @@ -168,7 +173,10 @@ public final class AccountingConstants { "incomeFromChargeOffFeesAccount"), CHARGE_OFF_EXPENSE( "chargeOffExpenseAccount"), CHARGE_OFF_FRAUD_EXPENSE( "chargeOffFraudExpenseAccount"), INCOME_FROM_CHARGE_OFF_PENALTY( - "incomeFromChargeOffPenaltyAccount"); + "incomeFromChargeOffPenaltyAccount"), INCOME_FROM_GOODWILL_CREDIT_INTEREST( + "incomeFromGoodwillCreditInterestAccount"), INCOME_FROM_GOODWILL_CREDIT_FEES( + "incomeFromGoodwillCreditFeesAccount"), INCOME_FROM_GOODWILL_CREDIT_PENALTY( + "incomeFromGoodwillCreditPenaltyAccount"); private final String value; 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 225047ee5..690672d1d 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 @@ -186,7 +186,9 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } else { accountMapForCredit.put(accountCredit, transactionPartAmount); } - Integer accountDebit = debitAccountType; + Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType, + accountMapForDebit); + if (accountMapForDebit.containsKey(accountDebit)) { BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); accountMapForDebit.put(accountDebit, amount); @@ -595,7 +597,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, @@ -624,7 +626,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), accountMapForCredit, accountMapForDebit); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), @@ -667,8 +669,8 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, - AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), - accountMapForCredit, accountMapForDebit); + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), accountMapForCredit, accountMapForDebit); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, @@ -780,6 +782,7 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess BigDecimal totalDebitAmount = new BigDecimal(0); Map accountMap = new LinkedHashMap<>(); + Map debitAccountMapForGoodwillCredit = new LinkedHashMap<>(); // handle principal payment or writeOff (and reversals) if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { @@ -787,6 +790,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); accountMap.put(account, principalAmount); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, principalAmount, AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), + debitAccountMapForGoodwillCredit, paymentTypeId); + } } // handle interest payment of writeOff (and reversals) @@ -800,6 +807,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } else { accountMap.put(account, interestAmount); } + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, interestAmount, + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), debitAccountMapForGoodwillCredit, + paymentTypeId); + } } // handle fees payment of writeOff (and reversals) @@ -821,6 +833,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess accountMap.put(account, feesAmount); } } + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, feesAmount, AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + debitAccountMapForGoodwillCredit, paymentTypeId); + } } // handle penalties payment of writeOff (and reversals) @@ -845,6 +861,12 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess accountMap.put(account, penaltiesAmount); } } + + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, penaltiesAmount, + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), debitAccountMapForGoodwillCredit, + paymentTypeId); + } } if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { @@ -857,6 +879,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } else { accountMap.put(account, overPaymentAmount); } + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, overPaymentAmount, AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), + debitAccountMapForGoodwillCredit, paymentTypeId); + } } for (Map.Entry entry : accountMap.entrySet()) { @@ -882,9 +908,12 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess transactionDate, totalDebitAmount, isReversal); } else { if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { - this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, - AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), loanProductId, paymentTypeId, loanId, transactionId, - transactionDate, totalDebitAmount, isReversal); + // create debit entries + for (Map.Entry debitEntry : debitAccountMapForGoodwillCredit.entrySet()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), + isReversal); + } } else { this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, @@ -907,6 +936,27 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } } + private void populateDebitAccountEntry(Long loanProductId, BigDecimal transactionPartAmount, Integer debitAccountType, + Map accountMapForDebit, Long paymentTypeId) { + Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType, + accountMapForDebit); + if (accountMapForDebit.containsKey(accountDebit)) { + BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); + accountMapForDebit.put(accountDebit, amount); + } else { + accountMapForDebit.put(accountDebit, transactionPartAmount); + } + } + + private Integer returnExistingDebitAccountInMapMatchingGLAccount(Long loanProductId, Long paymentTypeId, Integer accountType, + Map accountMap) { + GLAccount glAccount = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, accountType, paymentTypeId); + Integer accountEntry = accountMap.entrySet().stream().filter(account -> this.helper + .getLinkedGLAccountForLoanProduct(loanProductId, account.getKey(), paymentTypeId).getGlCode().equals(glAccount.getGlCode())) + .map(Map.Entry::getKey).findFirst().orElse(accountType); + return accountEntry; + } + /** * Create a single Debit to fund source and a single credit to "Income from Recovery" * diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java index f8bae9f62..6786c8a7f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/CashBasedAccountingProcessorForLoan.java @@ -195,7 +195,8 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } else { accountMapForCredit.put(accountCredit, transactionPartAmount); } - Integer accountDebit = debitAccountType; + Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType, + accountMapForDebit); if (accountMapForDebit.containsKey(accountDebit)) { BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); accountMapForDebit.put(accountDebit, amount); @@ -204,6 +205,15 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } } + private Integer returnExistingDebitAccountInMapMatchingGLAccount(Long loanProductId, Long paymentTypeId, Integer accountType, + Map accountMap) { + GLAccount glAccount = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, accountType, paymentTypeId); + Integer accountEntry = accountMap.entrySet().stream().filter(account -> this.helper + .getLinkedGLAccountForLoanProduct(loanProductId, account.getKey(), paymentTypeId).getGlCode().equals(glAccount.getGlCode())) + .map(Map.Entry::getKey).findFirst().orElse(accountType); + return accountEntry; + } + private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff(); if (isMarkedAsChargeOff) { @@ -609,7 +619,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); @@ -636,7 +646,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), accountMapForCredit, accountMapForDebit); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), @@ -664,7 +674,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), - CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), accountMapForCredit, accountMapForDebit); } else if (loanTransactionDTO.getTransactionType().isRepayment()) { populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), @@ -755,17 +765,27 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); BigDecimal totalDebitAmount = new BigDecimal(0); + Map debitAccountMapForGoodwillCredit = new LinkedHashMap<>(); if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(principalAmount); this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.LOAN_PORTFOLIO, loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalAmount, isReversal); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, principalAmount, CashAccountsForLoan.GOODWILL_CREDIT.getValue(), + debitAccountMapForGoodwillCredit, paymentTypeId); + } } if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(interestAmount); this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.INTEREST_ON_LOANS, loanProductId, paymentTypeId, loanId, transactionId, transactionDate, interestAmount, isReversal); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, interestAmount, + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), debitAccountMapForGoodwillCredit, + paymentTypeId); + } } if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { @@ -773,6 +793,10 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, CashAccountsForLoan.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, feesAmount, isReversal, loanTransactionDTO.getFeePayments()); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, feesAmount, CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + debitAccountMapForGoodwillCredit, paymentTypeId); + } } if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { @@ -780,12 +804,21 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), loanProductId, loanId, transactionId, transactionDate, penaltiesAmount, isReversal, loanTransactionDTO.getPenaltyPayments()); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, penaltiesAmount, + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), debitAccountMapForGoodwillCredit, + paymentTypeId); + } } if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(overPaymentAmount); this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.OVERPAYMENT, loanProductId, paymentTypeId, loanId, transactionId, transactionDate, overPaymentAmount, isReversal); + if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateDebitAccountEntry(loanProductId, overPaymentAmount, CashAccountsForLoan.GOODWILL_CREDIT.getValue(), + debitAccountMapForGoodwillCredit, paymentTypeId); + } } /*** create a single debit entry (or reversal) for the entire amount **/ @@ -797,8 +830,12 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); } else { if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { - this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.GOODWILL_CREDIT.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); + + // create debit entries + for (Map.Entry debitEntry : debitAccountMapForGoodwillCredit.entrySet()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + } } else { this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, CashAccountsForLoan.FUND_SOURCE.getValue(), @@ -818,6 +855,18 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF } } + private void populateDebitAccountEntry(Long loanProductId, BigDecimal transactionPartAmount, Integer debitAccountType, + Map accountMapForDebit, Long paymentTypeId) { + Integer accountDebit = returnExistingDebitAccountInMapMatchingGLAccount(loanProductId, paymentTypeId, debitAccountType, + accountMapForDebit); + if (accountMapForDebit.containsKey(accountDebit)) { + BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); + accountMapForDebit.put(accountDebit, amount); + } else { + accountMapForDebit.put(accountDebit, transactionPartAmount); + } + } + /** * Create a single Debit to fund source and a single credit to "Income from Recovery" * diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java index fc2c49615..20cfce149 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/LoanProductToGLAccountMappingHelper.java @@ -272,6 +272,15 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), loanProductId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + loanProductId, CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + loanProductId, CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + loanProductId, CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.toString(), changes); // expenses mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), loanProductId, @@ -330,6 +339,15 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), loanProductId, AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + loanProductId, AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + loanProductId, AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + loanProductId, AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.toString(), changes); // expenses mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), loanProductId, diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java index a9acb2bbb..930b27369 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingHelper.java @@ -89,6 +89,10 @@ public class ProductToGLAccountMappingHelper { optionalProductToGLAccountMappingEntries.add("chargeOffAccountId"); optionalProductToGLAccountMappingEntries.add("chargeOffFraudAccountId"); optionalProductToGLAccountMappingEntries.add("incomeFromChargeOffPenaltyAccountId"); + optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditInterestAccountId"); + optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditFeesAccountId"); + optionalProductToGLAccountMappingEntries.add("incomeFromGoodwillCreditPenaltyAccountId"); + if (optionalProductToGLAccountMappingEntries.contains(paramName)) { saveProductToAccountMapping(element, paramName, productId, accountTypeId, expectedAccountType, portfolioProductType); } else { diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java index c34e27c87..fa9bb087c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingReadPlatformServiceImpl.java @@ -148,7 +148,16 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product accountMappingDetails.put(LoanProductAccountingDataParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), gLAccountData); } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY)) { accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + gLAccountData); } + } } else if (AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingType) || AccountingRuleType.ACCRUAL_PERIODIC.getValue().equals(accountingType)) { @@ -198,6 +207,14 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product accountMappingDetails.put(LoanProductAccountingDataParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), gLAccountData); } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY)) { accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + gLAccountData); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java index c09d8ec64..ad957c5d0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/service/ProductToGLAccountMappingWritePlatformServiceImpl.java @@ -95,6 +95,15 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), loanProductId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), loanProductId, + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), loanProductId, + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), loanProductId, + CashAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue()); // expenses this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, @@ -164,6 +173,15 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), loanProductId, AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), loanProductId, + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), loanProductId, + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), loanProductId, + AccrualAccountsForLoan.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue()); // expenses this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 0fe3ac997..8c44abb38 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -123,7 +123,11 @@ public final class LoanProductDataValidator { LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), - LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), + LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), + LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.PRINCIPAL_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.INTEREST_RATE_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.NUMBER_OF_REPAYMENT_VARIATIONS_FOR_BORROWER_CYCLE_PARAMETER_NAME, LoanProductConstants.SHORT_NAME, @@ -658,6 +662,21 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()) .value(incomeFromChargeOffPenaltyAccountId).ignoreIfNull().integerGreaterThanZero(); + final Long incomeFromGoodwillCreditInterestAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue()) + .value(incomeFromGoodwillCreditInterestAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromGoodwillCreditFeesAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue()) + .value(incomeFromGoodwillCreditFeesAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromGoodwillCreditPenaltyAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue()) + .value(incomeFromGoodwillCreditPenaltyAccountId).ignoreIfNull().integerGreaterThanZero(); + final Long chargeOffExpenseAccountId = this.fromApiJsonHelper .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), element); baseDataValidator.reset().parameter(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue()).value(chargeOffExpenseAccountId) @@ -1528,6 +1547,21 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()) .value(incomeFromChargeOffPenaltyAccountId).ignoreIfNull().integerGreaterThanZero(); + final Long incomeFromGoodwillCreditInterestAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_INTEREST.getValue()) + .value(incomeFromGoodwillCreditInterestAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromGoodwillCreditFeesAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_FEES.getValue()) + .value(incomeFromGoodwillCreditFeesAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromGoodwillCreditPenaltyAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_GOODWILL_CREDIT_PENALTY.getValue()) + .value(incomeFromGoodwillCreditPenaltyAccountId).ignoreIfNull().integerGreaterThanZero(); + final Long chargeOffExpenseAccountId = this.fromApiJsonHelper .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), element); baseDataValidator.reset().parameter(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue()).value(chargeOffExpenseAccountId) diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java index df7637ebe..19bc77cda 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java @@ -429,6 +429,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); final Float overpaidAmount = 159.00f; final Float goodwillAmount = totalOutstanding + overpaidAmount; + final Float goodwillAmountInExpense = principalOutstanding + overpaidAmount; final String goodwillDate = "09 January 2022"; HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.makeRepaymentTypePayment(GOODWILL_CREDIT, goodwillDate, goodwillAmount, this.disbursedLoanID, ""); @@ -439,7 +440,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, goodwillDate, new JournalEntry(overpaidAmount, JournalEntry.TransactionType.CREDIT)); this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, goodwillDate, - new JournalEntry(goodwillAmount, JournalEntry.TransactionType.DEBIT)); + new JournalEntry(goodwillAmountInExpense, JournalEntry.TransactionType.DEBIT)); } @@ -455,6 +456,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); final Float overpaidAmount = 159.00f; final Float goodwillAmount = totalOutstanding + overpaidAmount; + final Float goodwillAmountInExpense = principalOutstanding + overpaidAmount; final String goodwillDate = "09 January 2022"; HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.makeRepaymentTypePayment(GOODWILL_CREDIT, goodwillDate, goodwillAmount, this.disbursedLoanID, ""); @@ -467,7 +469,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, goodwillDate, new JournalEntry(overpaidAmount, JournalEntry.TransactionType.CREDIT)); this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, goodwillDate, - new JournalEntry(goodwillAmount, JournalEntry.TransactionType.DEBIT)); + new JournalEntry(goodwillAmountInExpense, JournalEntry.TransactionType.DEBIT)); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeOffAccountingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeOffAccountingTest.java index 2ee0379a2..ccbfb0988 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeOffAccountingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeOffAccountingTest.java @@ -91,7 +91,7 @@ public class LoanChargeOffAccountingTest { // Product to GL account mapping for test // ASSET // -fundSourceAccountId,loanPortfolioAccountId,transfersInSuspenseAccountId,receivableFeeAccountId,receivablePenaltyAccountId,receivableInterestAccountId - // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId + // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId,incomeFromGoodwillCreditInterestAccountId,incomeFromGoodwillCreditFeesAccountId,incomeFromGoodwillCreditPenaltyAccountId // EXPENSE-writeOffAccountId,goodwillCreditAccountId,chargeOffExpenseAccountId,chargeOffFraudExpenseAccountId // LIABILITY-overpaymentLiabilityAccountId @@ -135,9 +135,7 @@ public class LoanChargeOffAccountingTest { this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", new JournalEntry(1000, JournalEntry.TransactionType.DEBIT)); this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", - new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); - this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", - new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); // make Repayment final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, @@ -246,7 +244,7 @@ public class LoanChargeOffAccountingTest { // Product to GL account mapping for test // ASSET // -fundSourceAccountId,loanPortfolioAccountId,transfersInSuspenseAccountId - // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId + // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId,incomeFromGoodwillCreditInterestAccountId,incomeFromGoodwillCreditFeesAccountId,incomeFromGoodwillCreditPenaltyAccountId // EXPENSE-writeOffAccountId,goodwillCreditAccountId,chargeOffExpenseAccountId,chargeOffFraudExpenseAccountId // LIABILITY-overpaymentLiabilityAccountId @@ -303,9 +301,7 @@ public class LoanChargeOffAccountingTest { this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", new JournalEntry(1000, JournalEntry.TransactionType.DEBIT)); this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", - new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); - this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", - new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); // make Repayment final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, @@ -411,6 +407,275 @@ public class LoanChargeOffAccountingTest { new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); } + // Tests for Goodwill Credit accounting changes + @Test + public void loanAccountingTreatmentTestForGoodwillCreditPeriodicAccrualAccounting_NoChargeOff() { + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Product to GL account mapping for test + // ASSET + // -fundSourceAccountId,loanPortfolioAccountId,transfersInSuspenseAccountId,receivableFeeAccountId,receivablePenaltyAccountId,receivableInterestAccountId + // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId,incomeFromGoodwillCreditInterestAccountId,incomeFromGoodwillCreditFeesAccountId,incomeFromGoodwillCreditPenaltyAccountId + // EXPENSE-writeOffAccountId,goodwillCreditAccountId,chargeOffExpenseAccountId,chargeOffFraudExpenseAccountId + // LIABILITY-overpaymentLiabilityAccountId + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccounting(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer feeLoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "110", true)); + + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + + // Goodwill Credit + final PostLoansLoanIdTransactionsResponse goodwillCredit_1 = loanTransactionHelper.makeGoodwillCredit((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(800.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + + // verify Journal Entries for Goodwill Credit + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "6 September 2022", + new JournalEntry(800, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", + new JournalEntry(780, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); + + } + + @Test + public void loanAccountingTreatmentTestForGoodwillCreditPeriodicAccrualAccounting_ChargeOff() { + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Product to GL account mapping for test + // ASSET + // -fundSourceAccountId,loanPortfolioAccountId,transfersInSuspenseAccountId,receivableFeeAccountId,receivablePenaltyAccountId,receivableInterestAccountId + // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId,incomeFromGoodwillCreditInterestAccountId,incomeFromGoodwillCreditFeesAccountId,incomeFromGoodwillCreditPenaltyAccountId + // EXPENSE-writeOffAccountId,goodwillCreditAccountId,chargeOffExpenseAccountId,chargeOffFraudExpenseAccountId + // LIABILITY-overpaymentLiabilityAccountId + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccounting(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer feeLoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + // set loan as chargeoff + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + String transactionExternalId = UUID.randomUUID().toString(); + this.loanTransactionHelper.chargeOffLoan((long) loanId, new PostLoansLoanIdTransactionsRequest().transactionDate("6 September 2022") + .locale("en").dateFormat("dd MMMM yyyy").externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries For ChargeOff Transaction + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "6 September 2022", + new JournalEntry(1020, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", + new JournalEntry(1000, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); + + // Goodwill Credit + final PostLoansLoanIdTransactionsResponse goodwillCredit_1 = loanTransactionHelper.makeGoodwillCredit((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("08 September 2022").locale("en") + .transactionAmount(800.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Goodwill Credit + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "8 September 2022", + new JournalEntry(800, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "8 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "8 September 2022", + new JournalEntry(780, JournalEntry.TransactionType.DEBIT)); + + } + + @Test + public void loanAccountingTreatmentTestForCashBasedAccounting_NoChargeOff() { + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Product to GL account mapping for test + // ASSET + // -fundSourceAccountId,loanPortfolioAccountId,transfersInSuspenseAccountId + // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId,incomeFromGoodwillCreditInterestAccountId,incomeFromGoodwillCreditFeesAccountId,incomeFromGoodwillCreditPenaltyAccountId + // EXPENSE-writeOffAccountId,goodwillCreditAccountId,chargeOffExpenseAccountId,chargeOffFraudExpenseAccountId + // LIABILITY-overpaymentLiabilityAccountId + + final Integer loanProductID = createLoanProductWithCashBasedAccounting(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer feeLoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + + // Goodwill Credit + final PostLoansLoanIdTransactionsResponse goodwillCredit_1 = loanTransactionHelper.makeGoodwillCredit((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("06 September 2022").locale("en") + .transactionAmount(800.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + + // verify Journal Entries for Goodwill Credit + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "6 September 2022", + new JournalEntry(780, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(10, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(10, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", + new JournalEntry(780, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); + + } + + @Test + public void loanAccountingTreatmentTestForCashBasedAccounting_ChargeOff() { + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // Product to GL account mapping for test + // ASSET + // -fundSourceAccountId,loanPortfolioAccountId,transfersInSuspenseAccountId + // INCOME-interestOnLoanAccountId,incomeFromFeeAccountId,incomeFromPenaltyAccountId,incomeFromRecoveryAccountId,incomeFromChargeOffInterestAccountId,incomeFromChargeOffFeesAccountId,incomeFromChargeOffPenaltyAccountId,incomeFromGoodwillCreditInterestAccountId,incomeFromGoodwillCreditFeesAccountId,incomeFromGoodwillCreditPenaltyAccountId + // EXPENSE-writeOffAccountId,goodwillCreditAccountId,chargeOffExpenseAccountId,chargeOffFraudExpenseAccountId + // LIABILITY-overpaymentLiabilityAccountId + + final Integer loanProductID = createLoanProductWithCashBasedAccounting(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + final Integer loanId = createLoanAccount(clientId, loanProductID, loanExternalIdStr); + + // apply charges + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + LocalDate targetDate = LocalDate.of(2022, 9, 5); + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer feeLoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(feeCharge), feeCharge1AddedDate, "10")); + + // apply penalty + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + + // set loan as chargeoff + String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5); + Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1); + String transactionExternalId = UUID.randomUUID().toString(); + this.loanTransactionHelper.chargeOffLoan((long) loanId, new PostLoansLoanIdTransactionsRequest().transactionDate("6 September 2022") + .locale("en").dateFormat("dd MMMM yyyy").externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries For ChargeOff Transaction + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "6 September 2022", + new JournalEntry(1000, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", + new JournalEntry(1000, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); + + // Goodwill Credit + final PostLoansLoanIdTransactionsResponse goodwillCredit_1 = loanTransactionHelper.makeGoodwillCredit((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("10 September 2022").locale("en") + .transactionAmount(800.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Goodwill Credit + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "6 September 2022", + new JournalEntry(1000, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "6 September 2022", + new JournalEntry(1000, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(20, JournalEntry.TransactionType.DEBIT)); + } + private Integer createLoanAccount(final Integer clientID, final Integer loanProductID, final String externalId) { String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 7a2d50b1e..d5f9fb637 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -446,6 +446,9 @@ public class LoanProductTestBuilder { map.put("incomeFromChargeOffInterestAccountId", ID); map.put("incomeFromChargeOffFeesAccountId", ID); map.put("incomeFromChargeOffPenaltyAccountId", ID); + map.put("incomeFromGoodwillCreditInterestAccountId", ID); + map.put("incomeFromGoodwillCreditFeesAccountId", ID); + map.put("incomeFromGoodwillCreditPenaltyAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.EXPENSE)) { final String ID = this.accountList[i].getAccountID().toString(); @@ -489,6 +492,9 @@ public class LoanProductTestBuilder { map.put("incomeFromChargeOffInterestAccountId", ID); map.put("incomeFromChargeOffFeesAccountId", ID); map.put("incomeFromChargeOffPenaltyAccountId", ID); + map.put("incomeFromGoodwillCreditInterestAccountId", ID); + map.put("incomeFromGoodwillCreditFeesAccountId", ID); + map.put("incomeFromGoodwillCreditPenaltyAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.EXPENSE)) { final String ID = this.accountList[i].getAccountID().toString();