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 d2e8a3372..dcc168565 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 @@ -37,7 +37,9 @@ public final class AccountingConstants { public enum CashAccountsForLoan { 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); + 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); private final Integer value; @@ -75,7 +77,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); + 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); private final Integer value; @@ -125,7 +129,12 @@ public final class AccountingConstants { "penaltyToIncomeAccountMappings"), CHARGE_ID( "chargeId"), INCOME_ACCOUNT_ID( "incomeAccountId"), INCOME_FROM_RECOVERY( - "incomeFromRecoveryAccountId"); + "incomeFromRecoveryAccountId"), INCOME_FROM_CHARGE_OFF_INTEREST( + "incomeFromChargeOffInterestAccountId"), INCOME_FROM_CHARGE_OFF_FEES( + "incomeFromChargeOffFeesAccountId"), CHARGE_OFF_EXPENSE( + "chargeOffExpenseAccountId"), CHARGE_OFF_FRAUD_EXPENSE( + "chargeOffFraudExpenseAccountId"), INCOME_FROM_CHARGE_OFF_PENALTY( + "incomeFromChargeOffPenaltyAccountId"); private final String value; @@ -154,7 +163,12 @@ public final class AccountingConstants { "transfersInSuspenseAccount"), INCOME_ACCOUNT_ID( "incomeAccount"), INCOME_FROM_RECOVERY( "incomeFromRecoveryAccount"), LIABILITY_TRANSFER_SUSPENSE( - "liabilityTransferInSuspenseAccount"); + "liabilityTransferInSuspenseAccount"), INCOME_FROM_CHARGE_OFF_INTEREST( + "incomeFromChargeOffInterestAccount"), INCOME_FROM_CHARGE_OFF_FEES( + "incomeFromChargeOffFeesAccount"), CHARGE_OFF_EXPENSE( + "chargeOffExpenseAccount"), CHARGE_OFF_FRAUD_EXPENSE( + "chargeOffFraudExpenseAccount"), INCOME_FROM_CHARGE_OFF_PENALTY( + "incomeFromChargeOffPenaltyAccount"); private final String value; diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java index 6bfc6ac48..60b50dc80 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java @@ -41,4 +41,8 @@ public class LoanDTO { private final boolean periodicAccrualBasedAccountingEnabled; @Setter private List newLoanTransactions; + @Setter + private boolean markedAsChargeOff; + @Setter + private boolean markedAsFraud; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java index 1f7e8b656..fec88b4f4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java @@ -108,6 +108,8 @@ public class AccountingProcessorHelper { final String currencyCode = (String) accountingBridgeData.get("currencyCode"); final List newLoanTransactions = new ArrayList<>(); boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer"); + boolean isLoanMarkedAsChargeOff = (Boolean) accountingBridgeData.get("isChargeOff"); + boolean isLoanMarkedAsFraud = (Boolean) accountingBridgeData.get("isFraud"); @SuppressWarnings("unchecked") final List> newTransactionsMap = (List>) accountingBridgeData.get("newLoanTransactions"); @@ -162,7 +164,7 @@ public class AccountingProcessorHelper { } return new LoanDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled, - periodicAccrualBasedAccountingEnabled, newLoanTransactions); + periodicAccrualBasedAccountingEnabled, newLoanTransactions, isLoanMarkedAsChargeOff, isLoanMarkedAsFraud); } public SavingsDTO populateSavingsDtoFromMap(final Map accountingBridgeData, final boolean cashBasedAccountingEnabled, 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 5c729eb6c..225047ee5 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 @@ -104,10 +104,208 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) { createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office); } + // Logic for Charge-Off + else if (loanTransactionDTO.getTransactionType().isChargeoff()) { + createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office); + } + } + } + + private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, 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 Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + final boolean isReversal = loanTransactionDTO.isReversed(); + + Map accountMapForCredit = new LinkedHashMap<>(); + + Map accountMapForDebit = new LinkedHashMap<>(); + + // principal payment + if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), + AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), accountMapForCredit, accountMapForDebit); + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), + AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), accountMapForCredit, accountMapForDebit); + } + } + + // interest payment + if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { + + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + } + + // handle fees payment + if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), accountMapForCredit, accountMapForDebit); + } + + // handle penalty payment + if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), accountMapForCredit, accountMapForDebit); + } + + // create credit entries + for (Map.Entry creditEntry : accountMapForCredit.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + creditEntry.getValue(), isReversal, creditEntry.getKey()); + } + + // create debit entries + for (Map.Entry debitEntry : accountMapForDebit.entrySet()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + } + + } + + private void populateCreditDebitMaps(Long loanProductId, BigDecimal transactionPartAmount, Long paymentTypeId, + Integer creditAccountType, Integer debitAccountType, Map accountMapForCredit, + Map accountMapForDebit) { + GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, creditAccountType, paymentTypeId); + if (accountMapForCredit.containsKey(accountCredit)) { + BigDecimal amount = accountMapForCredit.get(accountCredit).add(transactionPartAmount); + accountMapForCredit.put(accountCredit, amount); + } else { + accountMapForCredit.put(accountCredit, transactionPartAmount); + } + Integer accountDebit = debitAccountType; + if (accountMapForDebit.containsKey(accountDebit)) { + BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); + accountMapForDebit.put(accountDebit, amount); + } else { + accountMapForDebit.put(accountDebit, transactionPartAmount); } } private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { + final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff(); + if (isMarkedAsChargeOff) { + createJournalEntriesForChargeOffLoanChargeAdjustment(loanDTO, loanTransactionDTO, office); + } else { + createJournalEntriesForLoanChargeAdjustment(loanDTO, loanTransactionDTO, office); + } + } + + private void createJournalEntriesForChargeOffLoanChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, + 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(); + final boolean isReversal = loanTransactionDTO.isReversed(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + Map accountMap = new LinkedHashMap<>(); + + // handle principal payment + if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), paymentTypeId); + accountMap.put(account, principalAmount); + } + + // handle interest payment + if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(interestAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, interestAmount); + } + + } + + // handle fees payment + if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(feesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, feesAmount); + } + } + + // handle penalty payment + if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, penaltiesAmount); + } + } + + // handle overpayment + if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(overPaymentAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, overPaymentAmount); + } + } + + for (Map.Entry entry : accountMap.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + entry.getValue(), isReversal, entry.getKey()); + } + + if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) { + Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId(); + Integer accountMappingTypeId; + if (loanTransactionDTO.getLoanChargeData().isPenalty()) { + accountMappingTypeId = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); + } else { + accountMappingTypeId = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(); + } + this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId, + loanId, transactionId, transactionDate, totalDebitAmount, isReversal); + } + } + + private void createJournalEntriesForLoanChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); @@ -306,6 +504,263 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess */ private void createJournalEntriesForRepaymentsAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office, final boolean writeOff, final boolean isIncomeFromFee) { + final boolean isMarkedChargeOff = loanDTO.isMarkedAsChargeOff(); + if (isMarkedChargeOff) { + createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee); + + } else { + createJournalEntriesForLoansRepaymentAndWriteOffs(loanDTO, loanTransactionDTO, office, writeOff, isIncomeFromFee); + } + } + + private void createJournalEntriesForChargeOffLoanRepaymentAndWriteOffs(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, + Office office, boolean writeOff, boolean isIncomeFromFee) { + // 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 boolean isReversal = loanTransactionDTO.isReversed(); + + Map accountMapForCredit = new LinkedHashMap<>(); + Map accountMapForDebit = new LinkedHashMap<>(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + // principal payment + if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // interest payment + if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // handle fees payment + if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + if (isIncomeFromFee) { + this.helper.createCreditJournalEntryOrReversalForLoanCharges(office, currencyCode, + AccrualAccountsForLoan.INCOME_FROM_FEES.getValue(), loanProductId, loanId, transactionId, transactionDate, + feesAmount, isReversal, loanTransactionDTO.getFeePayments()); + Integer accountDebit = AccrualAccountsForLoan.FUND_SOURCE.getValue(); + if (accountMapForDebit.containsKey(accountDebit)) { + BigDecimal amount = accountMapForDebit.get(accountDebit).add(feesAmount); + accountMapForDebit.put(accountDebit, amount); + } else { + accountMapForDebit.put(accountDebit, feesAmount); + } + + } else { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + } + + // handle penalties + if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else { + if (isIncomeFromFee) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } else { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), AccrualAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } + } + + } + + // overpayment + if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(), + AccrualAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // create credit entries + for (Map.Entry creditEntry : accountMapForCredit.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + creditEntry.getValue(), isReversal, creditEntry.getKey()); + } + + if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) { + if (writeOff) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, + AccrualAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(), loanProductId, paymentTypeId, loanId, transactionId, + transactionDate, totalDebitAmount, isReversal); + } else { + if (loanTransactionDTO.isLoanToLoanTransfer()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); + } else if (loanTransactionDTO.isAccountTransfer()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, + FinancialActivity.LIABILITY_TRANSFER.getValue(), loanProductId, paymentTypeId, loanId, transactionId, + transactionDate, totalDebitAmount, isReversal); + } else { + // create debit entries + for (Map.Entry debitEntry : accountMapForDebit.entrySet()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + } + } + } + } + + /** + * Charge Refunds (and their reversals) have an extra refund related pair of journal entries in addition to + * those related to the repayment above + ***/ + if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) { + if (loanTransactionDTO.getTransactionType().isChargeRefund()) { + Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType()); + this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, incomeAccount, + AccrualAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, + totalDebitAmount, isReversal); + } + } + + } + + private void createJournalEntriesForLoansRepaymentAndWriteOffs(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, + final Office office, final boolean writeOff, final boolean isIncomeFromFee) { // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); 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 60cb1c7dc..f8bae9f62 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 @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.closure.domain.GLClosure; -import org.apache.fineract.accounting.common.AccountingConstants; import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan; import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; import org.apache.fineract.accounting.glaccount.domain.GLAccount; @@ -115,10 +114,108 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) { createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office); } + // Logic for Charge-Off + else if (loanTransactionDTO.getTransactionType().isChargeoff()) { + createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office); + } + } + } + + private void createJournalEntriesForChargeOff(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, 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 Long paymentTypeId = loanTransactionDTO.getPaymentTypeId(); + final boolean isReversal = loanTransactionDTO.isReversed(); + + Map accountMapForCredit = new LinkedHashMap<>(); + + Map accountMapForDebit = new LinkedHashMap<>(); + + // principal payment + if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), + CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), accountMapForCredit, accountMapForDebit); + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), + CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), accountMapForCredit, accountMapForDebit); + } + } + + // interest payment + if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + } + + // handle fees payment + if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_FEES.getValue(), + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), accountMapForCredit, accountMapForDebit); + } + + // handle penalties payment + if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), accountMapForCredit, accountMapForDebit); + } + + // create credit entries + for (Map.Entry creditEntry : accountMapForCredit.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + creditEntry.getValue(), isReversal, creditEntry.getKey()); + } + + // create debit entries + for (Map.Entry debitEntry : accountMapForDebit.entrySet()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + } + + } + + private void populateCreditDebitMaps(Long loanProductId, BigDecimal transactionPartAmount, Long paymentTypeId, + Integer creditAccountType, Integer debitAccountType, Map accountMapForCredit, + Map accountMapForDebit) { + GLAccount accountCredit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, creditAccountType, paymentTypeId); + if (accountMapForCredit.containsKey(accountCredit)) { + BigDecimal amount = accountMapForCredit.get(accountCredit).add(transactionPartAmount); + accountMapForCredit.put(accountCredit, amount); + } else { + accountMapForCredit.put(accountCredit, transactionPartAmount); + } + Integer accountDebit = debitAccountType; + if (accountMapForDebit.containsKey(accountDebit)) { + BigDecimal amount = accountMapForDebit.get(accountDebit).add(transactionPartAmount); + accountMapForDebit.put(accountDebit, amount); + } else { + accountMapForDebit.put(accountDebit, transactionPartAmount); } } private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { + final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff(); + if (isMarkedAsChargeOff) { + createJournalEntriesForChargeOffLoanChargeAdjustment(loanDTO, loanTransactionDTO, office); + } else { + createJournalEntriesForLoanChargeAdjustment(loanDTO, loanTransactionDTO, office); + } + + } + + private void createJournalEntriesForChargeOffLoanChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, + Office office) { // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); @@ -143,7 +240,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(principalAmount); GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, - AccountingConstants.CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId); + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), paymentTypeId); accountMap.put(account, principalAmount); } @@ -151,7 +248,7 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(interestAmount); GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, - AccountingConstants.CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), paymentTypeId); + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), paymentTypeId); if (accountMap.containsKey(account)) { BigDecimal amount = accountMap.get(account).add(interestAmount); accountMap.put(account, amount); @@ -164,7 +261,106 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(feesAmount); GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, - AccountingConstants.CashAccountsForLoan.INCOME_FROM_FEES.getValue(), paymentTypeId); + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(feesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, feesAmount); + } + } + + // handle penalties payment + if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(penaltiesAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, penaltiesAmount); + } + } + + // handle overpayment + if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, CashAccountsForLoan.OVERPAYMENT.getValue(), + paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(overPaymentAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, overPaymentAmount); + } + } + + for (Map.Entry entry : accountMap.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + entry.getValue(), isReversal, entry.getKey()); + } + + if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) { + Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId(); + Integer accountMappingTypeId; + if (loanTransactionDTO.getLoanChargeData().isPenalty()) { + accountMappingTypeId = CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); + } else { + accountMappingTypeId = CashAccountsForLoan.INCOME_FROM_FEES.getValue(); + } + this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId, + loanId, transactionId, transactionDate, totalDebitAmount, isReversal); + } + } + + private void createJournalEntriesForLoanChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, 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(); + final boolean isReversal = loanTransactionDTO.isReversed(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + Map accountMap = new LinkedHashMap<>(); + + // handle principal payment (and reversals) + if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), + paymentTypeId); + accountMap.put(account, principalAmount); + } + + // handle interest payment (and reversals) + if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, + CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), paymentTypeId); + if (accountMap.containsKey(account)) { + BigDecimal amount = accountMap.get(account).add(interestAmount); + accountMap.put(account, amount); + } else { + accountMap.put(account, interestAmount); + } + } + + // handle fees payment (and reversals) + if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, CashAccountsForLoan.INCOME_FROM_FEES.getValue(), + paymentTypeId); if (accountMap.containsKey(account)) { BigDecimal amount = accountMap.get(account).add(feesAmount); accountMap.put(account, amount); @@ -189,8 +385,8 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF // handle overpayment if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { totalDebitAmount = totalDebitAmount.add(overPaymentAmount); - GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, - AccountingConstants.CashAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId); + GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, CashAccountsForLoan.OVERPAYMENT.getValue(), + paymentTypeId); if (accountMap.containsKey(account)) { BigDecimal amount = accountMap.get(account).add(overPaymentAmount); accountMap.put(account, amount); @@ -208,9 +404,9 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId(); Integer accountMappingTypeId; if (loanTransactionDTO.getLoanChargeData().isPenalty()) { - accountMappingTypeId = AccountingConstants.CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); + accountMappingTypeId = CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(); } else { - accountMappingTypeId = AccountingConstants.CashAccountsForLoan.INCOME_FROM_FEES.getValue(); + accountMappingTypeId = CashAccountsForLoan.INCOME_FROM_FEES.getValue(); } this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); @@ -324,6 +520,224 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF */ private void createJournalEntriesForRepayments(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) { + + final boolean isMarkedChargeOff = loanDTO.isMarkedAsChargeOff(); + if (isMarkedChargeOff) { + createJournalEntriesForChargeOffLoanRepayments(loanDTO, loanTransactionDTO, office); + + } else { + createJournalEntriesForLoanRepayments(loanDTO, loanTransactionDTO, office); + } + + } + + private void createJournalEntriesForChargeOffLoanRepayments(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, 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 boolean isReversal = loanTransactionDTO.isReversed(); + + Map accountMapForCredit = new LinkedHashMap<>(); + Map accountMapForDebit = new LinkedHashMap<>(); + + BigDecimal totalDebitAmount = new BigDecimal(0); + + // principal payment + if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(principalAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + if (isMarkedFraud) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, + CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + } + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // interest payment + if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(interestAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), accountMapForCredit, accountMapForDebit); + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // handle fees payment + if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(feesAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_FEES.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // handle penalties payment + if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(penaltiesAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), CashAccountsForLoan.FUND_SOURCE.getValue(), + accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId, CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // overpayment + if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { + totalDebitAmount = totalDebitAmount.add(overPaymentAmount); + if (loanTransactionDTO.getTransactionType().isMerchantIssuedRefund()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isPayoutRefund()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else if (loanTransactionDTO.getTransactionType().isGoodwillCredit()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), + CashAccountsForLoan.GOODWILL_CREDIT.getValue(), accountMapForCredit, accountMapForDebit); + } else if (loanTransactionDTO.getTransactionType().isRepayment()) { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + + } else { + populateCreditDebitMaps(loanProductId, overPaymentAmount, paymentTypeId, CashAccountsForLoan.OVERPAYMENT.getValue(), + CashAccountsForLoan.FUND_SOURCE.getValue(), accountMapForCredit, accountMapForDebit); + } + + } + + // create credit entries + for (Map.Entry creditEntry : accountMapForCredit.entrySet()) { + this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate, + creditEntry.getValue(), isReversal, creditEntry.getKey()); + } + + /*** create a single debit entry (or reversal) for the entire amount **/ + if (loanTransactionDTO.isLoanToLoanTransfer()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.ASSET_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); + } else if (loanTransactionDTO.isAccountTransfer()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, FinancialActivity.LIABILITY_TRANSFER.getValue(), + loanProductId, paymentTypeId, loanId, transactionId, transactionDate, totalDebitAmount, isReversal); + } else { + // create debit entries + for (Map.Entry debitEntry : accountMapForDebit.entrySet()) { + this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal); + } + } + + /** + * Charge Refunds (and their reversals) have an extra refund related pair of journal entries in addition to + * those related to the repayment above + ***/ + if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) { + if (loanTransactionDTO.getTransactionType().isChargeRefund()) { + Integer incomeAccount = this.helper.getValueForFeeOrPenaltyIncomeAccount(loanTransactionDTO.getChargeRefundChargeType()); + this.helper.createJournalEntriesAndReversalsForLoan(office, currencyCode, incomeAccount, + CashAccountsForLoan.FUND_SOURCE.getValue(), loanProductId, paymentTypeId, loanId, transactionId, transactionDate, + totalDebitAmount, isReversal); + } + } + } + + private void createJournalEntriesForLoanRepayments(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) { // loan properties final Long loanProductId = loanDTO.getLoanProductId(); final Long loanId = loanDTO.getLoanId(); 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 e7514deb7..fc2c49615 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 @@ -263,12 +263,26 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi changes); mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), loanProductId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), CashAccountsForLoan.INCOME_FROM_RECOVERY.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), + loanProductId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), + loanProductId, CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.toString(), changes); + 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); // expenses mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), loanProductId, CashAccountsForLoan.LOSSES_WRITTEN_OFF.getValue(), CashAccountsForLoan.LOSSES_WRITTEN_OFF.toString(), changes); mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), loanProductId, CashAccountsForLoan.GOODWILL_CREDIT.getValue(), CashAccountsForLoan.GOODWILL_CREDIT.toString(), changes); + mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), loanProductId, + CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), CashAccountsForLoan.CHARGE_OFF_EXPENSE.toString(), changes); + mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), + loanProductId, CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), + CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.toString(), changes); // liabilities mergeLoanToLiabilityAccountMappingChanges(element, LoanProductAccountingParams.OVERPAYMENT.getValue(), loanProductId, @@ -307,6 +321,15 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), loanProductId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue(), AccrualAccountsForLoan.INCOME_FROM_RECOVERY.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), + loanProductId, AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(), + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.toString(), changes); + mergeLoanToIncomeAccountMappingChanges(element, LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), + loanProductId, AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.toString(), changes); + 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); // expenses mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), loanProductId, @@ -314,6 +337,12 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi changes); mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), loanProductId, AccrualAccountsForLoan.GOODWILL_CREDIT.getValue(), AccrualAccountsForLoan.GOODWILL_CREDIT.toString(), changes); + mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), loanProductId, + AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(), AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.toString(), + changes); + mergeLoanToExpenseAccountMappingChanges(element, LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), + loanProductId, AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(), + AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.toString(), changes); // liabilities mergeLoanToLiabilityAccountMappingChanges(element, LoanProductAccountingParams.OVERPAYMENT.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 3b594ba56..a9acb2bbb 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 @@ -84,6 +84,11 @@ public class ProductToGLAccountMappingHelper { if (accountMapping == null) { ArrayList optionalProductToGLAccountMappingEntries = new ArrayList(); optionalProductToGLAccountMappingEntries.add("goodwillCreditAccountId"); + optionalProductToGLAccountMappingEntries.add("incomeFromChargeOffInterestAccountId"); + optionalProductToGLAccountMappingEntries.add("incomeFromChargeOffFeesAccountId"); + optionalProductToGLAccountMappingEntries.add("chargeOffAccountId"); + optionalProductToGLAccountMappingEntries.add("chargeOffFraudAccountId"); + optionalProductToGLAccountMappingEntries.add("incomeFromChargeOffPenaltyAccountId"); 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 5cf3f735d..c34e27c87 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 @@ -138,6 +138,16 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product accountMappingDetails.put(LoanProductAccountingDataParams.OVERPAYMENT.getValue(), gLAccountData); } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_RECOVERY)) { accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_RECOVERY.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.CHARGE_OFF_EXPENSE)) { + accountMappingDetails.put(LoanProductAccountingDataParams.CHARGE_OFF_EXPENSE.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE)) { + 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 (AccountingRuleType.ACCRUAL_UPFRONT.getValue().equals(accountingType) @@ -178,6 +188,16 @@ public class ProductToGLAccountMappingReadPlatformServiceImpl implements Product accountMappingDetails.put(LoanProductAccountingDataParams.PENALTIES_RECEIVABLE.getValue(), gLAccountData); } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_RECOVERY)) { accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_RECOVERY.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST)) { + accountMappingDetails.put(LoanProductAccountingDataParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.CHARGE_OFF_EXPENSE)) { + accountMappingDetails.put(LoanProductAccountingDataParams.CHARGE_OFF_EXPENSE.getValue(), gLAccountData); + } else if (glAccountForLoan.equals(AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE)) { + 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); } } 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 41d94d28b..c09d8ec64 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 @@ -86,6 +86,15 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), loanProductId, CashAccountsForLoan.INCOME_FROM_RECOVERY.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), loanProductId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), loanProductId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), loanProductId, + CashAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()); // expenses this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, @@ -94,6 +103,12 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), loanProductId, CashAccountsForLoan.GOODWILL_CREDIT.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, + LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), loanProductId, + CashAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, + LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), loanProductId, + CashAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue()); // liabilities this.loanProductToGLAccountMappingHelper.saveLoanToLiabilityAccountMapping(element, @@ -140,6 +155,15 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), loanProductId, AccrualAccountsForLoan.INCOME_FROM_RECOVERY.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToIncomeAccountMapping(element, + LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), loanProductId, + AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()); // expenses this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, @@ -148,6 +172,12 @@ public class ProductToGLAccountMappingWritePlatformServiceImpl implements Produc this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), loanProductId, AccrualAccountsForLoan.GOODWILL_CREDIT.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, + LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), loanProductId, + AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue()); + this.loanProductToGLAccountMappingHelper.saveLoanToExpenseAccountMapping(element, + LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), loanProductId, + AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue()); // liabilities this.loanProductToGLAccountMappingHelper.saveLoanToLiabilityAccountMapping(element, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java index e86183190..47ed70bf7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java @@ -53,6 +53,7 @@ public class LoanTransactionEnumData { private final boolean creditBalanceRefund; private final boolean chargeAdjustment; private final boolean chargeback; + private final boolean chargeoff; public LoanTransactionEnumData(final Long id, final String code, final String value) { this.id = id; @@ -81,6 +82,7 @@ public class LoanTransactionEnumData { this.creditBalanceRefund = Long.valueOf(20).equals(this.id); this.chargeback = Long.valueOf(25).equals(this.id); this.chargeAdjustment = Long.valueOf(26).equals(this.id); + this.chargeoff = Long.valueOf(27).equals(this.id); } public boolean isRepaymentType() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 4de3ed85c..d66428e6b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -4526,6 +4526,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", isUpfrontAccrualAccountingEnabledOnLoanProduct()); accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", isPeriodicAccrualAccountingEnabledOnLoanProduct()); accountingBridgeData.put("isAccountTransfer", isAccountTransfer); + accountingBridgeData.put("isChargeOff", isChargedOff()); + accountingBridgeData.put("isFraud", isFraud()); final List> newLoanTransactions = new ArrayList<>(); for (final LoanTransaction transaction : this.loanTransactions) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java index 26090f0bb..f0c46d89c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualWritePlatformServiceImpl.java @@ -277,7 +277,7 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - public Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, + private Map deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, final Map transactionMap) { final Map accountingBridgeData = new LinkedHashMap<>(); @@ -289,6 +289,8 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", false); accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", true); accountingBridgeData.put("isAccountTransfer", false); + accountingBridgeData.put("isChargeOff", false); + accountingBridgeData.put("isFraud", false); final List> newLoanTransactions = new ArrayList<>(); newLoanTransactions.add(transactionMap); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 53400439a..aba8b4476 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -2675,8 +2675,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.markAsChargedOff(transactionDate, currentUser, null); } + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId); - loanTransactionRepository.saveAndFlush(chargeOffTransaction); + loan.addLoanTransaction(chargeOffTransaction); + saveAndFlushLoanWithDataIntegrityViolationChecks(loan); String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParameterName); if (StringUtils.isNotBlank(noteText)) { @@ -2685,7 +2689,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.noteRepository.save(note); } - // TODO: add accounting + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction)); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // 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 bcdd51a9d..0fe3ac997 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 @@ -119,7 +119,11 @@ public final class LoanProductDataValidator { LoanProductAccountingParams.GOODWILL_CREDIT.getValue(), LoanProductAccountingParams.PENALTIES_RECEIVABLE.getValue(), LoanProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), LoanProductAccountingParams.FEE_INCOME_ACCOUNT_MAPPING.getValue(), LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), - LoanProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME, + LoanProductAccountingParams.PENALTY_INCOME_ACCOUNT_MAPPING.getValue(), + 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, 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, @@ -639,6 +643,31 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductAccountingParams.OVERPAYMENT.getValue()).value(overpaymentAccountId).notNull() .integerGreaterThanZero(); + final Long incomeFromChargeOffInterestAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue()) + .value(incomeFromChargeOffInterestAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromChargeOffFeesAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue()) + .value(incomeFromChargeOffFeesAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromChargeOffPenaltyAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()) + .value(incomeFromChargeOffPenaltyAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long chargeOffExpenseAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue()).value(chargeOffExpenseAccountId) + .ignoreIfNull().integerGreaterThanZero(); + + final Long chargeOffFraudExpenseAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue()) + .value(chargeOffFraudExpenseAccountId).ignoreIfNull().integerGreaterThanZero(); + validatePaymentChannelFundSourceMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element); @@ -1484,6 +1513,31 @@ public final class LoanProductDataValidator { baseDataValidator.reset().parameter(LoanProductAccountingParams.PENALTIES_RECEIVABLE.getValue()).value(receivablePenaltyAccountId) .ignoreIfNull().integerGreaterThanZero(); + final Long incomeFromChargeOffInterestAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_INTEREST.getValue()) + .value(incomeFromChargeOffInterestAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromChargeOffFeesAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_FEES.getValue()) + .value(incomeFromChargeOffFeesAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long incomeFromChargeOffPenaltyAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_CHARGE_OFF_PENALTY.getValue()) + .value(incomeFromChargeOffPenaltyAccountId).ignoreIfNull().integerGreaterThanZero(); + + final Long chargeOffExpenseAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.CHARGE_OFF_EXPENSE.getValue()).value(chargeOffExpenseAccountId) + .ignoreIfNull().integerGreaterThanZero(); + + final Long chargeOffFraudExpenseAccountId = this.fromApiJsonHelper + .extractLongNamed(LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue(), element); + baseDataValidator.reset().parameter(LoanProductAccountingParams.CHARGE_OFF_FRAUD_EXPENSE.getValue()) + .value(chargeOffFraudExpenseAccountId).ignoreIfNull().integerGreaterThanZero(); + validatePaymentChannelFundSourceMappings(baseDataValidator, element); validateChargeToIncomeAccountMappings(baseDataValidator, element); 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 new file mode 100644 index 000000000..2ee0379a2 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeOffAccountingTest.java @@ -0,0 +1,450 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.UUID; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PutLoansLoanIdResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.accounting.JournalEntry; +import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper; +import org.apache.fineract.integrationtests.common.charges.ChargesHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.system.CodeHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class LoanChargeOffAccountingTest { + + private ResponseSpecification responseSpec; + private ResponseSpecification responseSpec403; + private RequestSpecification requestSpec; + private ClientHelper clientHelper; + private LoanTransactionHelper loanTransactionHelper; + private LoanTransactionHelper loanTransactionHelperValidationError; + private JournalEntryHelper journalEntryHelper; + private AccountHelper accountHelper; + private Account assetAccount; + private Account incomeAccount; + private Account expenseAccount; + private Account overpaymentAccount; + private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.responseSpec403 = new ResponseSpecBuilder().expectStatusCode(403).build(); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + this.loanTransactionHelperValidationError = new LoanTransactionHelper(this.requestSpec, new ResponseSpecBuilder().build()); + this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + this.assetAccount = this.accountHelper.createAssetAccount(); + this.incomeAccount = this.accountHelper.createIncomeAccount(); + this.expenseAccount = this.accountHelper.createExpenseAccount(); + this.overpaymentAccount = this.accountHelper.createLiabilityAccount(); + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void loanChargeOffAccountingTreatmentTestForPeriodicAccrualAccounting() { + // 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 + // 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(10, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); + + // make Repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") + .transactionAmount(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Repayment transaction + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "7 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "7 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.DEBIT)); + + // Merchant Refund + final PostLoansLoanIdTransactionsResponse merchantIssuedRefund_1 = loanTransactionHelper.makeMerchantIssuedRefund((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("8 September 2022").locale("en") + .transactionAmount(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Merchant Refund + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "8 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "8 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.DEBIT)); + + // Payout Refund + final PostLoansLoanIdTransactionsResponse payoutRefund_1 = loanTransactionHelper.makePayoutRefund((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("9 September 2022").locale("en") + .transactionAmount(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Payout Refund + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "9 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "9 September 2022", + new JournalEntry(100, 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(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Goodwill Credit + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "10 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "10 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + + // make overpaid repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("11 September 2022").locale("en") + .transactionAmount(720.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getOverpaid()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal entries for overpaid repayment + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, "11 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "11 September 2022", + new JournalEntry(620, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "11 September 2022", + new JournalEntry(720, JournalEntry.TransactionType.DEBIT)); + + // CBR for making loan active again + final PostLoansLoanIdTransactionsResponse cbr_transaction = loanTransactionHelper.makeCreditBalanceRefund(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("12 September 2022").locale("en") + .transactionAmount(100.0)); + + // Charge Adjustment making loan overpaid + final PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResult = loanTransactionHelper.chargeAdjustment((long) loanId, + (long) feeLoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(10.0).locale("en")); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getOverpaid()); + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + String transactionDate = Utils.dateFormatter.format(todaysDate); + + // verify Journal entries for Charge Adjustment + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, transactionDate, + new JournalEntry(10, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, transactionDate, + new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); + } + + @Test + public void loanChargeOffFraudAccountingTreatmentTestForCashBasedAccounting() { + // 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 + // 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")); + + // set loan as fraud + final String command = "markAsFraud"; + String payload = loanTransactionHelper.getLoanFraudPayloadAsJSON("fraud", "true"); + PutLoansLoanIdResponse putLoansLoanIdResponse = loanTransactionHelper.modifyLoanCommand(loanId, command, payload, + this.responseSpec); + + GetLoansLoanIdResponse loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getFraud()); + + // 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.getFraud()); + 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(10, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "6 September 2022", + new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); + + // make Repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("7 September 2022").locale("en") + .transactionAmount(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getFraud()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Repayment transaction + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "7 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "7 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.DEBIT)); + + // Merchant Refund + final PostLoansLoanIdTransactionsResponse merchantIssuedRefund_1 = loanTransactionHelper.makeMerchantIssuedRefund((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("8 September 2022").locale("en") + .transactionAmount(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getFraud()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Merchant Refund + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "8 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "8 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.DEBIT)); + + // Payout Refund + final PostLoansLoanIdTransactionsResponse payoutRefund_1 = loanTransactionHelper.makePayoutRefund((long) loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("9 September 2022").locale("en") + .transactionAmount(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getFraud()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Payout Refund + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "9 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "9 September 2022", + new JournalEntry(100, 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(100.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getActive()); + assertTrue(loanDetails.getFraud()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal Entries for Goodwill Credit + this.journalEntryHelper.checkJournalEntryForExpenseAccount(expenseAccount, "10 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.DEBIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "10 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + + // make overpaid repayment + final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("11 September 2022").locale("en") + .transactionAmount(720.0)); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getOverpaid()); + assertTrue(loanDetails.getFraud()); + assertTrue(loanDetails.getChargedOff()); + + // verify Journal entries for overpaid repayment + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, "11 September 2022", + new JournalEntry(100, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, "11 September 2022", + new JournalEntry(620, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "11 September 2022", + new JournalEntry(720, JournalEntry.TransactionType.DEBIT)); + + // CBR for making loan active again + final PostLoansLoanIdTransactionsResponse cbr_transaction = loanTransactionHelper.makeCreditBalanceRefund(loanExternalIdStr, + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("12 September 2022").locale("en") + .transactionAmount(100.0)); + + // Charge Adjustment making loan overpaid + final PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResult = loanTransactionHelper.chargeAdjustment((long) loanId, + (long) feeLoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(10.0).locale("en")); + + loanDetails = this.loanTransactionHelper.getLoanDetails((long) loanId); + assertTrue(loanDetails.getStatus().getOverpaid()); + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + String transactionDate = Utils.dateFormatter.format(todaysDate); + + // verify Journal entries for Charge Adjustment + this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, transactionDate, + new JournalEntry(10, JournalEntry.TransactionType.CREDIT)); + this.journalEntryHelper.checkJournalEntryForIncomeAccount(incomeAccount, transactionDate, + new JournalEntry(10, JournalEntry.TransactionType.DEBIT)); + } + + private Integer createLoanAccount(final Integer clientID, final Integer loanProductID, final String externalId) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance() + .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate("03 September 2022").withSubmittedOnDate("01 September 2022").withLoanType("individual") + .withExternalId(externalId).build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan("02 September 2022", "1000", loanId, null); + loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanId, "1000"); + return loanId; + } + + private Integer createLoanProductWithPeriodicAccrualAccounting(final Account... accounts) { + + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentAfterEvery("1") + .withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat() + .withAccountingRulePeriodicAccrual(accounts).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0") + .build(null); + + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + + private Integer createLoanProductWithCashBasedAccounting(final Account... accounts) { + + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentAfterEvery("1") + .withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat() + .withAccountingRuleAsCashBased(accounts).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0").build(null); + + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + +} 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 adb4700d1..7f0eb32cc 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 @@ -443,11 +443,16 @@ public class LoanProductTestBuilder { map.put("incomeFromFeeAccountId", ID); map.put("incomeFromPenaltyAccountId", ID); map.put("incomeFromRecoveryAccountId", ID); + map.put("incomeFromChargeOffInterestAccountId", ID); + map.put("incomeFromChargeOffFeesAccountId", ID); + map.put("incomeFromChargeOffPenaltyAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.EXPENSE)) { final String ID = this.accountList[i].getAccountID().toString(); map.put("writeOffAccountId", ID); map.put("goodwillCreditAccountId", ID); + map.put("chargeOffExpenseAccountId", ID); + map.put("chargeOffFraudExpenseAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.LIABILITY)) { final String ID = this.accountList[i].getAccountID().toString(); @@ -481,11 +486,16 @@ public class LoanProductTestBuilder { map.put("incomeFromFeeAccountId", ID); map.put("incomeFromPenaltyAccountId", ID); map.put("incomeFromRecoveryAccountId", ID); + map.put("incomeFromChargeOffInterestAccountId", ID); + map.put("incomeFromChargeOffFeesAccountId", ID); + map.put("incomeFromChargeOffPenaltyAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.EXPENSE)) { final String ID = this.accountList[i].getAccountID().toString(); map.put("writeOffAccountId", ID); map.put("goodwillCreditAccountId", ID); + map.put("chargeOffExpenseAccountId", ID); + map.put("chargeOffFraudExpenseAccountId", ID); } if (this.accountList[i].getAccountType().equals(Account.AccountType.LIABILITY)) { final String ID = this.accountList[i].getAccountID().toString();