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 d92657b7b..51bab9942 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 @@ -451,33 +451,54 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess } if (principalCredited.compareTo(principalPaid) > 0) { - helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalCredited.subtract(principalPaid), - isReversal); + helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, getPrincipalAccount(loanDTO), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, principalCredited.subtract(principalPaid), isReversal); } else if (principalCredited.compareTo(principalPaid) < 0) { - helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPaid.subtract(principalCredited), - isReversal); + helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, getPrincipalAccount(loanDTO), loanProductId, + paymentTypeId, loanId, transactionId, transactionDate, principalPaid.subtract(principalCredited), isReversal); } if (feeCredited.compareTo(feePaid) > 0) { - helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, feeCredited.subtract(feePaid), isReversal); + helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, getFeeAccount(loanDTO), loanProductId, paymentTypeId, + loanId, transactionId, transactionDate, feeCredited.subtract(feePaid), isReversal); } else if (feeCredited.compareTo(feePaid) < 0) { - helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, feePaid.subtract(feeCredited), isReversal); + helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, getFeeAccount(loanDTO), loanProductId, paymentTypeId, + loanId, transactionId, transactionDate, feePaid.subtract(feeCredited), isReversal); } if (penaltyCredited.compareTo(penaltyPaid) > 0) { - helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, penaltyCredited.subtract(penaltyPaid), - isReversal); + helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, getPenaltyAccount(loanDTO), loanProductId, paymentTypeId, + loanId, transactionId, transactionDate, penaltyCredited.subtract(penaltyPaid), isReversal); } else if (penaltyCredited.compareTo(penaltyPaid) < 0) { - helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), - loanProductId, paymentTypeId, loanId, transactionId, transactionDate, penaltyPaid.subtract(penaltyCredited), - isReversal); + helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, getPenaltyAccount(loanDTO), loanProductId, paymentTypeId, + loanId, transactionId, transactionDate, penaltyPaid.subtract(penaltyCredited), isReversal); } + } + private Integer getFeeAccount(LoanDTO loanDTO) { + Integer account = AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(); + if (loanDTO.isMarkedAsChargeOff()) { + account = AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES.getValue(); + } + return account; + } + + private Integer getPenaltyAccount(LoanDTO loanDTO) { + Integer account = AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(); + if (loanDTO.isMarkedAsChargeOff()) { + account = AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY.getValue(); + } + return account; + } + + private Integer getPrincipalAccount(LoanDTO loanDTO) { + if (loanDTO.isMarkedAsFraud() && loanDTO.isMarkedAsChargeOff()) { + return AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE.getValue(); + } else if (!loanDTO.isMarkedAsFraud() && loanDTO.isMarkedAsChargeOff()) { + return AccrualAccountsForLoan.CHARGE_OFF_EXPENSE.getValue(); + } else { + return AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(); + } } /** diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 7903ca41e..a45d07ef2 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -23,6 +23,7 @@ import static java.lang.Boolean.TRUE; import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE; import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -68,6 +69,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.models.PutLoansLoanIdResponse; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.BatchHelper; import org.apache.fineract.integrationtests.common.BusinessDateHelper; @@ -85,6 +87,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductHelper; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.system.CodeHelper; import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper; import org.apache.fineract.integrationtests.useradministration.users.UserHelper; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -140,11 +143,13 @@ public abstract class BaseLoanIntegrationTest { protected final Account feeIncomeAccount = accountHelper.createIncomeAccount("feeIncome"); protected final Account penaltyIncomeAccount = accountHelper.createIncomeAccount("penaltyIncome"); protected final Account feeChargeOffAccount = accountHelper.createIncomeAccount("feeChargeOff"); + protected final Account penaltyChargeOffAccount = accountHelper.createIncomeAccount("penaltyChargeOff"); + protected final Account recoveriesAccount = accountHelper.createIncomeAccount("recoveries"); protected final Account interestIncomeChargeOffAccount = accountHelper.createIncomeAccount("interestIncomeChargeOff"); // expense - protected final Account creditLossBadDebtAccount = accountHelper.createExpenseAccount(); - protected final Account creditLossBadDebtFraudAccount = accountHelper.createExpenseAccount(); + protected final Account chargeOffExpenseAccount = accountHelper.createExpenseAccount("chargeOff"); + protected final Account chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud"); protected final Account writtenOffAccount = accountHelper.createExpenseAccount(); protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount(); @@ -233,9 +238,10 @@ public abstract class BaseLoanIntegrationTest { .incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())// .incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())// .incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())// - .chargeOffExpenseAccountId(creditLossBadDebtAccount.getAccountID().longValue())// - .chargeOffFraudExpenseAccountId(creditLossBadDebtFraudAccount.getAccountID().longValue())// - .incomeFromChargeOffPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue()).dateFormat(DATETIME_PATTERN)// + .incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())// + .chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())// + .chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())// + .dateFormat(DATETIME_PATTERN)// .locale("en_GB")// .disallowExpectedDisbursements(true)// .allowApprovedDisbursedAmountsOverApplied(true)// @@ -398,7 +404,7 @@ public abstract class BaseLoanIntegrationTest { protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) { GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertNotNull(loanDetails.getLastClosedBusinessDate()); + assertNotNull(loanDetails.getLastClosedBusinessDate()); Assertions.assertEquals(lastClosedBusinessDate, loanDetails.getLastClosedBusinessDate().format(dateTimeFormatter)); } @@ -438,10 +444,10 @@ public abstract class BaseLoanIntegrationTest { protected Long addCharge(Long loanId, boolean isPenalty, double amount, String dueDate) { Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(amount), isPenalty)); - Assertions.assertNotNull(chargeId); + assertNotNull(chargeId); Integer loanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId.intValue(), LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(chargeId), dueDate, String.valueOf(amount))); - Assertions.assertNotNull(loanChargeId); + assertNotNull(loanChargeId); return loanChargeId.longValue(); } @@ -449,8 +455,8 @@ public abstract class BaseLoanIntegrationTest { GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN); - Assertions.assertNotNull(loanResponse.getRepaymentSchedule()); - Assertions.assertNotNull(loanResponse.getRepaymentSchedule().getPeriods()); + assertNotNull(loanResponse.getRepaymentSchedule()); + assertNotNull(loanResponse.getRepaymentSchedule().getPeriods()); Assertions.assertEquals(installments.length, loanResponse.getRepaymentSchedule().getPeriods().size(), "Expected installments are not matching with the installments configured on the loan"); @@ -621,6 +627,24 @@ public abstract class BaseLoanIntegrationTest { return response.getResourceId(); } + protected Long chargeOffLoan(Long loanId, String date) { + 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(); + + PostLoansLoanIdTransactionsResponse chargeOffTransaction = this.loanTransactionHelper.chargeOffLoan((long) loanId, + new PostLoansLoanIdTransactionsRequest().transactionDate(date).locale("en").dateFormat("dd MMMM yyyy") + .externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId)); + return chargeOffTransaction.getResourceId(); + } + + protected void changeLoanFraudState(Long loanId, boolean fraudState) { + String payload = loanTransactionHelper.getLoanFraudPayloadAsJSON("fraud", fraudState ? "true" : "false"); + PutLoansLoanIdResponse response = loanTransactionHelper.modifyLoanCommand(Math.toIntExact(loanId), "markAsFraud", payload, + responseSpec); + assertNotNull(response); + } + protected Long addChargebackForLoan(Long loanId, Long transactionId, Double amount) { PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.chargebackLoanTransaction(loanId, transactionId, new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(amount).paymentTypeId(1L)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java index ccb3e1a2a..15986d92f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java @@ -1460,6 +1460,321 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa }); } + @Test + public void testAccountingChargebackOnChargeOffWithPrincipal() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId, 3); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(750), "01 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 February 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 March 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 April 2023") // + ); + + // Repayment #1 + updateBusinessDate("01 February 2023"); + Long repaymentTransaction1 = addRepaymentForLoan(loanId, 250.0, "01 February 2023"); + + // Repayment #2 + updateBusinessDate("01 March 2023"); + Long repaymentTransaction2 = addRepaymentForLoan(loanId, 250.0, "01 March 2023"); + + // Charge-Off + updateBusinessDate("15 March 2023"); + Long chargeOff = chargeOffLoan(loanId, "15 March 2023"); + + // Chargeback 250 + updateBusinessDate("30 March 2023"); + Long chargeback = addChargebackForLoan(loanId, repaymentTransaction2, 250.0); + + verifyTransactions(loanId, // + transaction(750.0, "Disbursement", "01 January 2023", 750.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Repayment", "01 February 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Repayment", "01 March 2023", 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Charge-off", "15 March 2023", 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Chargeback", "30 March 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // Verify GL entries + verifyTRJournalEntries(repaymentTransaction1, // + debit(fundSource, 250), // + credit(loansReceivableAccount, 250)// + ); + + verifyTRJournalEntries(repaymentTransaction2, // + debit(fundSource, 250), // + credit(loansReceivableAccount, 250) // + ); + + verifyTRJournalEntries(chargeOff, // + debit(chargeOffExpenseAccount, 250), // + credit(loansReceivableAccount, 250)// + ); + + verifyTRJournalEntries(chargeback, // + debit(chargeOffExpenseAccount, 250), // + credit(fundSource, 250) // + ); + }); + } + + @Test + public void testAccountingChargebackOnChargeOffFraudWithPrincipal() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId, 3); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(750), "01 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 February 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 March 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 April 2023") // + ); + + // Repayment #1 + updateBusinessDate("01 February 2023"); + Long repaymentTransaction1 = addRepaymentForLoan(loanId, 250.0, "01 February 2023"); + + // Repayment #2 + updateBusinessDate("01 March 2023"); + Long repaymentTransaction2 = addRepaymentForLoan(loanId, 250.0, "01 March 2023"); + + // Charge-Off + updateBusinessDate("15 March 2023"); + Long chargeOff = chargeOffLoan(loanId, "15 March 2023"); + changeLoanFraudState(loanId, true); + + // Chargeback 250 + updateBusinessDate("30 March 2023"); + Long chargeback = addChargebackForLoan(loanId, repaymentTransaction2, 250.0); + + verifyTransactions(loanId, // + transaction(750.0, "Disbursement", "01 January 2023", 750.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Repayment", "01 February 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Repayment", "01 March 2023", 250.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Charge-off", "15 March 2023", 0.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Chargeback", "30 March 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + // Verify GL entries + verifyTRJournalEntries(repaymentTransaction1, // + debit(fundSource, 250), // + credit(loansReceivableAccount, 250)// + ); + + verifyTRJournalEntries(repaymentTransaction2, // + debit(fundSource, 250), // + credit(loansReceivableAccount, 250) // + ); + + verifyTRJournalEntries(chargeOff, // + debit(chargeOffExpenseAccount, 250), // + credit(loansReceivableAccount, 250)// + ); + + verifyTRJournalEntries(chargeback, // + debit(chargeOffFraudExpenseAccount, 250), // + credit(fundSource, 250) // + ); + }); + } + + @Test + public void testAccountingChargebackOnChargeOffWithFees() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId, 3); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(750), "01 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 February 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 March 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 April 2023") // + ); + + // Repayment #1 + updateBusinessDate("01 February 2023"); + Long repaymentTransaction1 = addRepaymentForLoan(loanId, 250.0, "01 February 2023"); + + // Add fee 30 + updateBusinessDate("01 March 2023"); + addCharge(loanId, false, 30, "01 March 2023"); + + // Repayment #2 + Long repaymentTransaction2 = addRepaymentForLoan(loanId, 280.0, "01 March 2023"); + + // Run periodic accrual + schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions"); + + // Charge-Off + updateBusinessDate("15 March 2023"); + addCharge(loanId, false, 20, "15 March 2023"); + Long chargeOff = chargeOffLoan(loanId, "15 March 2023"); + + // Chargeback 250 + updateBusinessDate("30 March 2023"); + Long chargeback = addChargebackForLoan(loanId, repaymentTransaction2, 280.0); + + verifyTransactions(loanId, // + transaction(750.0, "Disbursement", "01 January 2023", 750.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Repayment", "01 February 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(30.0, "Accrual", "01 March 2023", 0.0, 0.0, 0.0, 30.0, 0.0, 0.0, 0.0), // + transaction(280.0, "Repayment", "01 March 2023", 250.0, 250.0, 0.0, 30.0, 0.0, 0.0, 0.0), // + transaction(270.0, "Charge-off", "15 March 2023", 0.0, 250.0, 0.0, 20.0, 0.0, 0.0, 0.0), // + transaction(280.0, "Chargeback", "30 March 2023", 500.0, 250.0, 0.0, 30.0, 0.0, 0.0, 0.0) // + ); + + // Verify GL entries + verifyTRJournalEntries(repaymentTransaction1, // + debit(fundSource, 250), // + credit(loansReceivableAccount, 250)// + ); + + verifyTRJournalEntries(repaymentTransaction2, // + debit(fundSource, 280), // + credit(loansReceivableAccount, 250), // + credit(feeReceivableAccount, 30) // + ); + + verifyTRJournalEntries(getTransactionId(loanId, "Accrual", "01 March 2023"), // + debit(feeReceivableAccount, 30), // + credit(feeIncomeAccount, 30) // + ); + + verifyTRJournalEntries(chargeOff, // + debit(chargeOffExpenseAccount, 250), // + credit(loansReceivableAccount, 250), // + credit(feeReceivableAccount, 20), // + debit(feeChargeOffAccount, 20) // + ); + + verifyTRJournalEntries(chargeback, // + credit(fundSource, 280), // + debit(chargeOffExpenseAccount, 250), // + debit(feeChargeOffAccount, 30) // + ); + }); + } + + @Test + public void testAccountingChargebackOnChargeOffWithPenalties() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + // Create Loan Product + Long loanProductId = createLoanProduct(// + createDefaultPaymentAllocation(), // + chargebackAllocation("PENALTY", "FEE", "INTEREST", "PRINCIPAL")// + ); + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId, 3); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(750), "01 January 2023"); + + verifyRepaymentSchedule(loanId, // + installment(0, null, "01 January 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 February 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 March 2023"), // + installment(250.0, 0, 0, 0, 250.0, false, "01 April 2023") // + ); + + // Repayment #1 + updateBusinessDate("01 February 2023"); + Long repaymentTransaction1 = addRepaymentForLoan(loanId, 250.0, "01 February 2023"); + + // Add fee 30 + updateBusinessDate("01 March 2023"); + addCharge(loanId, true, 30, "01 March 2023"); + + // Repayment #2 + Long repaymentTransaction2 = addRepaymentForLoan(loanId, 280.0, "01 March 2023"); + + // Run periodic accrual + schedulerJobHelper.executeAndAwaitJob("Add Accrual Transactions"); + + // Charge-Off + updateBusinessDate("15 March 2023"); + addCharge(loanId, true, 20, "15 March 2023"); + Long chargeOff = chargeOffLoan(loanId, "15 March 2023"); + + // Chargeback 250 + updateBusinessDate("30 March 2023"); + Long chargeback = addChargebackForLoan(loanId, repaymentTransaction2, 280.0); + + verifyTransactions(loanId, // + transaction(750.0, "Disbursement", "01 January 2023", 750.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Repayment", "01 February 2023", 500.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(30.0, "Accrual", "01 March 2023", 0.0, 0.0, 0.0, 0.0, 30.0, 0.0, 0.0), // + transaction(280.0, "Repayment", "01 March 2023", 250.0, 250.0, 0.0, 0.0, 30.0, 0.0, 0.0), // + transaction(270.0, "Charge-off", "15 March 2023", 0.0, 250.0, 0.0, 0.0, 20.0, 0.0, 0.0), // + transaction(280.0, "Chargeback", "30 March 2023", 500.0, 250.0, 0.0, 0.0, 30.0, 0.0, 0.0) // + ); + + // Verify GL entries + verifyTRJournalEntries(repaymentTransaction1, // + debit(fundSource, 250), // + credit(loansReceivableAccount, 250)// + ); + + verifyTRJournalEntries(repaymentTransaction2, // + debit(fundSource, 280), // + credit(loansReceivableAccount, 250), // + credit(penaltyReceivableAccount, 30) // + ); + + verifyTRJournalEntries(getTransactionId(loanId, "Accrual", "01 March 2023"), // + debit(penaltyReceivableAccount, 30), // + credit(penaltyIncomeAccount, 30) // + ); + + verifyTRJournalEntries(chargeOff, // + debit(chargeOffExpenseAccount, 250), // + credit(loansReceivableAccount, 250), // + credit(penaltyReceivableAccount, 20), // + debit(penaltyChargeOffAccount, 20) // + ); + + verifyTRJournalEntries(chargeback, // + credit(fundSource, 280), // + debit(chargeOffExpenseAccount, 250), // + debit(penaltyChargeOffAccount, 30) // + ); + }); + } + private void verifyLoanSummaryAmounts(Long loanId, double creditedPrincipal, double creditedFee, double creditedPenalty, double totalOutstanding) { GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); @@ -1471,7 +1786,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa Assertions.assertEquals(totalOutstanding, summary.getTotalOutstanding()); } - @Nullable private Long applyAndApproveLoan(Long clientId, Long loanProductId, int numberOfRepayments) { PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, numberOfRepayments)// .repaymentEvery(1)// @@ -1484,7 +1798,8 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(1250.0, "01 January 2023")); - + Assertions.assertNotNull(approvedLoanResult); + Assertions.assertNotNull(approvedLoanResult.getLoanId()); return approvedLoanResult.getLoanId(); }