FINERACT-2042: chargeback with chargeoff
This commit is contained in:
committed by
Peter Bagrij
parent
18697f7cd6
commit
99f89b2d8a
+37
-16
@@ -451,33 +451,54 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (principalCredited.compareTo(principalPaid) > 0) {
|
if (principalCredited.compareTo(principalPaid) > 0) {
|
||||||
helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
|
helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, getPrincipalAccount(loanDTO), loanProductId,
|
||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalCredited.subtract(principalPaid),
|
paymentTypeId, loanId, transactionId, transactionDate, principalCredited.subtract(principalPaid), isReversal);
|
||||||
isReversal);
|
|
||||||
} else if (principalCredited.compareTo(principalPaid) < 0) {
|
} else if (principalCredited.compareTo(principalPaid) < 0) {
|
||||||
helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
|
helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, getPrincipalAccount(loanDTO), loanProductId,
|
||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, principalPaid.subtract(principalCredited),
|
paymentTypeId, loanId, transactionId, transactionDate, principalPaid.subtract(principalCredited), isReversal);
|
||||||
isReversal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feeCredited.compareTo(feePaid) > 0) {
|
if (feeCredited.compareTo(feePaid) > 0) {
|
||||||
helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(),
|
helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, getFeeAccount(loanDTO), loanProductId, paymentTypeId,
|
||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, feeCredited.subtract(feePaid), isReversal);
|
loanId, transactionId, transactionDate, feeCredited.subtract(feePaid), isReversal);
|
||||||
} else if (feeCredited.compareTo(feePaid) < 0) {
|
} else if (feeCredited.compareTo(feePaid) < 0) {
|
||||||
helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(),
|
helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, getFeeAccount(loanDTO), loanProductId, paymentTypeId,
|
||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, feePaid.subtract(feeCredited), isReversal);
|
loanId, transactionId, transactionDate, feePaid.subtract(feeCredited), isReversal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (penaltyCredited.compareTo(penaltyPaid) > 0) {
|
if (penaltyCredited.compareTo(penaltyPaid) > 0) {
|
||||||
helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(),
|
helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, getPenaltyAccount(loanDTO), loanProductId, paymentTypeId,
|
||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, penaltyCredited.subtract(penaltyPaid),
|
loanId, transactionId, transactionDate, penaltyCredited.subtract(penaltyPaid), isReversal);
|
||||||
isReversal);
|
|
||||||
} else if (penaltyCredited.compareTo(penaltyPaid) < 0) {
|
} else if (penaltyCredited.compareTo(penaltyPaid) < 0) {
|
||||||
helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(),
|
helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, getPenaltyAccount(loanDTO), loanProductId, paymentTypeId,
|
||||||
loanProductId, paymentTypeId, loanId, transactionId, transactionDate, penaltyPaid.subtract(penaltyCredited),
|
loanId, transactionId, transactionDate, penaltyPaid.subtract(penaltyCredited), isReversal);
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+34
-10
@@ -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.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.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.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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.PostLoansLoanIdTransactionsTransactionIdRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansRequest;
|
import org.apache.fineract.client.models.PostLoansRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
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.client.util.CallFailedRuntimeException;
|
||||||
import org.apache.fineract.integrationtests.common.BatchHelper;
|
import org.apache.fineract.integrationtests.common.BatchHelper;
|
||||||
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
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.LoanProductTestBuilder;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
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.inlinecob.InlineLoanCOBHelper;
|
||||||
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
|
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
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 feeIncomeAccount = accountHelper.createIncomeAccount("feeIncome");
|
||||||
protected final Account penaltyIncomeAccount = accountHelper.createIncomeAccount("penaltyIncome");
|
protected final Account penaltyIncomeAccount = accountHelper.createIncomeAccount("penaltyIncome");
|
||||||
protected final Account feeChargeOffAccount = accountHelper.createIncomeAccount("feeChargeOff");
|
protected final Account feeChargeOffAccount = accountHelper.createIncomeAccount("feeChargeOff");
|
||||||
|
protected final Account penaltyChargeOffAccount = accountHelper.createIncomeAccount("penaltyChargeOff");
|
||||||
|
|
||||||
protected final Account recoveriesAccount = accountHelper.createIncomeAccount("recoveries");
|
protected final Account recoveriesAccount = accountHelper.createIncomeAccount("recoveries");
|
||||||
protected final Account interestIncomeChargeOffAccount = accountHelper.createIncomeAccount("interestIncomeChargeOff");
|
protected final Account interestIncomeChargeOffAccount = accountHelper.createIncomeAccount("interestIncomeChargeOff");
|
||||||
// expense
|
// expense
|
||||||
protected final Account creditLossBadDebtAccount = accountHelper.createExpenseAccount();
|
protected final Account chargeOffExpenseAccount = accountHelper.createExpenseAccount("chargeOff");
|
||||||
protected final Account creditLossBadDebtFraudAccount = accountHelper.createExpenseAccount();
|
protected final Account chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
|
||||||
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
|
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
|
||||||
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
|
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
|
||||||
|
|
||||||
@@ -233,9 +238,10 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
.chargeOffExpenseAccountId(creditLossBadDebtAccount.getAccountID().longValue())//
|
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
|
||||||
.chargeOffFraudExpenseAccountId(creditLossBadDebtFraudAccount.getAccountID().longValue())//
|
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
|
||||||
.incomeFromChargeOffPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue()).dateFormat(DATETIME_PATTERN)//
|
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
|
||||||
|
.dateFormat(DATETIME_PATTERN)//
|
||||||
.locale("en_GB")//
|
.locale("en_GB")//
|
||||||
.disallowExpectedDisbursements(true)//
|
.disallowExpectedDisbursements(true)//
|
||||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||||
@@ -398,7 +404,7 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) {
|
protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) {
|
||||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
Assertions.assertNotNull(loanDetails.getLastClosedBusinessDate());
|
assertNotNull(loanDetails.getLastClosedBusinessDate());
|
||||||
Assertions.assertEquals(lastClosedBusinessDate, loanDetails.getLastClosedBusinessDate().format(dateTimeFormatter));
|
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) {
|
protected Long addCharge(Long loanId, boolean isPenalty, double amount, String dueDate) {
|
||||||
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
|
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||||
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(amount), isPenalty));
|
ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(amount), isPenalty));
|
||||||
Assertions.assertNotNull(chargeId);
|
assertNotNull(chargeId);
|
||||||
Integer loanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId.intValue(),
|
Integer loanChargeId = this.loanTransactionHelper.addChargesForLoan(loanId.intValue(),
|
||||||
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(chargeId), dueDate, String.valueOf(amount)));
|
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(chargeId), dueDate, String.valueOf(amount)));
|
||||||
Assertions.assertNotNull(loanChargeId);
|
assertNotNull(loanChargeId);
|
||||||
return loanChargeId.longValue();
|
return loanChargeId.longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,8 +455,8 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||||
|
|
||||||
Assertions.assertNotNull(loanResponse.getRepaymentSchedule());
|
assertNotNull(loanResponse.getRepaymentSchedule());
|
||||||
Assertions.assertNotNull(loanResponse.getRepaymentSchedule().getPeriods());
|
assertNotNull(loanResponse.getRepaymentSchedule().getPeriods());
|
||||||
Assertions.assertEquals(installments.length, loanResponse.getRepaymentSchedule().getPeriods().size(),
|
Assertions.assertEquals(installments.length, loanResponse.getRepaymentSchedule().getPeriods().size(),
|
||||||
"Expected installments are not matching with the installments configured on the loan");
|
"Expected installments are not matching with the installments configured on the loan");
|
||||||
|
|
||||||
@@ -621,6 +627,24 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
return response.getResourceId();
|
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) {
|
protected Long addChargebackForLoan(Long loanId, Long transactionId, Double amount) {
|
||||||
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.chargebackLoanTransaction(loanId, transactionId,
|
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.chargebackLoanTransaction(loanId, transactionId,
|
||||||
new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(amount).paymentTypeId(1L));
|
new PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(amount).paymentTypeId(1L));
|
||||||
|
|||||||
+317
-2
@@ -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,
|
private void verifyLoanSummaryAmounts(Long loanId, double creditedPrincipal, double creditedFee, double creditedPenalty,
|
||||||
double totalOutstanding) {
|
double totalOutstanding) {
|
||||||
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||||
@@ -1471,7 +1786,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
Assertions.assertEquals(totalOutstanding, summary.getTotalOutstanding());
|
Assertions.assertEquals(totalOutstanding, summary.getTotalOutstanding());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Long applyAndApproveLoan(Long clientId, Long loanProductId, int numberOfRepayments) {
|
private Long applyAndApproveLoan(Long clientId, Long loanProductId, int numberOfRepayments) {
|
||||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, numberOfRepayments)//
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", 1250.0, numberOfRepayments)//
|
||||||
.repaymentEvery(1)//
|
.repaymentEvery(1)//
|
||||||
@@ -1484,7 +1798,8 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
|||||||
|
|
||||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
approveLoanRequest(1250.0, "01 January 2023"));
|
approveLoanRequest(1250.0, "01 January 2023"));
|
||||||
|
Assertions.assertNotNull(approvedLoanResult);
|
||||||
|
Assertions.assertNotNull(approvedLoanResult.getLoanId());
|
||||||
return approvedLoanResult.getLoanId();
|
return approvedLoanResult.getLoanId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user