This is an automated email from the ASF dual-hosted git repository.
bagrijp pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 99f89b2d8 FINERACT-2042: chargeback with chargeoff
99f89b2d8 is described below
commit 99f89b2d8acd2bcbe0afd245955b981b93f996e4
Author: Peter Bagrij <[email protected]>
AuthorDate: Fri Mar 8 09:23:57 2024 +0100
FINERACT-2042: chargeback with chargeoff
---
.../AccrualBasedAccountingProcessorForLoan.java | 53 ++--
.../integrationtests/BaseLoanIntegrationTest.java | 44 ++-
...ebackWithCreditAllocationsIntegrationTests.java | 319 ++++++++++++++++++++-
3 files changed, 388 insertions(+), 28 deletions(-)
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();
}