FINERACT-2104: Accrual Activity Reverse Replay Transaction

This commit is contained in:
Soma Sörös
2024-07-15 10:31:28 +02:00
committed by Adam Saghy
parent a4f077d675
commit 9fceaf98b3
8 changed files with 398 additions and 24 deletions
@@ -2481,24 +2481,19 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public List<LoanTransaction> retrieveListOfTransactionsForReprocessing() {
return getLoanTransactions().stream()
.filter(transaction -> transaction.isNotReversed() && !transaction.isAccrual()
&& (transaction.isChargeOff() || transaction.isReAge() || transaction.isReAmortize()
|| !transaction.isNonMonetaryTransaction()))
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
return getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate()).sorted(LoanTransactionComparator.INSTANCE)
.collect(Collectors.toList());
}
public List<LoanTransaction> retrieveListOfTransactionsPostDisbursementExcludeAccruals() {
return this.loanTransactions.stream()
.filter(transaction -> transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isRepaymentAtDisbursement()
|| transaction.isNonMonetaryTransaction() || transaction.isIncomePosting()))
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
private static Predicate<LoanTransaction> loanTransactionForReprocessingPredicate() {
return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge()
|| transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction());
}
private List<LoanTransaction> retrieveListOfTransactionsExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isNonMonetaryTransaction())) {
if (transaction.isNotReversed() && !transaction.isNonMonetaryTransaction()) {
repaymentsOrWaivers.add(transaction);
}
}
@@ -3901,6 +3896,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return currentTransactionDate;
}
public LoanTransaction getLastTransactionForReprocessing() {
return loanTransactions.stream() //
.filter(Loan.loanTransactionForReprocessingPredicate()) //
.reduce((first, second) -> second) //
.orElse(null);
}
public LoanTransaction getLastPaymentTransaction() {
return loanTransactions.stream() //
.filter(loanTransaction -> !loanTransaction.isReversed()) //
@@ -4526,7 +4528,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
outstanding = outstanding.plus(transactionOutstanding);
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
} else {
} else if (!loanTransaction.isAccrualActivity()) {
if (this.loanInterestRecalculationDetails != null
&& this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& !loanTransaction.isRepaymentAtDisbursement()) {
@@ -879,11 +879,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public boolean isNonMonetaryTransaction() {
return isNotReversed() && (LoanTransactionType.CONTRA.equals(getTypeOf())
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf())
|| LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf())
|| LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf()) || LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf())
|| LoanTransactionType.CHARGE_OFF.equals(getTypeOf()) || LoanTransactionType.REAMORTIZE.equals(getTypeOf())
|| LoanTransactionType.REAGE.equals(getTypeOf()));
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) || LoanTransactionType.ACCRUAL.equals(getTypeOf())
|| LoanTransactionType.ACCRUAL_ACTIVITY.equals(getTypeOf()) || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf())
|| LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf())
|| LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()) || LoanTransactionType.CHARGE_OFF.equals(getTypeOf())
|| LoanTransactionType.REAMORTIZE.equals(getTypeOf()) || LoanTransactionType.REAGE.equals(getTypeOf()));
}
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
@@ -949,7 +949,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
}
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment();
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment() || isAccrualActivity();
}
public boolean isAccrualTransaction() {
@@ -969,8 +969,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
}
public boolean isPaymentTransaction() {
return this.isNotReversed() && !(this.isDisbursement() || this.isAccrual() || this.isRepaymentAtDisbursement()
|| this.isNonMonetaryTransaction() || this.isIncomePosting());
return this.isNotReversed() && !(this.isDisbursement() || this.isRepaymentAtDisbursement() || this.isNonMonetaryTransaction()
|| this.isIncomePosting());
}
public Set<LoanCollateralManagement> getLoanCollateralManagementSet() {
@@ -204,12 +204,43 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed);
} else if (loanTransaction.isChargeOff()) {
recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments);
} else if (loanTransaction.isAccrualActivity()) {
recalculateAccrualActivityTransaction(changedTransactionDetail, loanTransaction, currency, installments);
}
}
reprocessInstallments(disbursementDate, transactionsToBeProcessed, installments, currency);
return changedTransactionDetail;
}
protected void calculateAccrualActivity(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) {
loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges
final Money principalPortion = Money.zero(currency);
Money interestPortion = Money.zero(currency);
Money feeChargesPortion = Money.zero(currency);
Money penaltychargesPortion = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (loanTransaction.getDateOf().isEqual(currentInstallment.getDueDate())) {
interestPortion = interestPortion.plus(currentInstallment.getInterestCharged(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesCharged(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
}
}
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
}
private void recalculateAccrualActivityTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments) {
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
calculateAccrualActivity(newLoanTransaction, currency, installments);
if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
}
}
@Override
public void processLatestTransaction(final LoanTransaction loanTransaction, final TransactionCtx ctx) {
switch (loanTransaction.getTypeOf()) {
@@ -2785,7 +2785,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, onDate, calculateTill);
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals();
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsForReprocessing();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(),
loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges());
@@ -197,6 +197,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case REAGE -> handleReAge(loanTransaction, ctx);
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
// TODO: Cover rest of the transaction types
default -> {
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
@@ -57,6 +57,10 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
}
}
private boolean isAccrualActivity() {
return loanTransaction.isPresent() && loanTransaction.get().isAccrualActivity();
}
private boolean isBackdatedCharge() {
return loanCharge.get().getDueDate().isBefore(loanCharge.get().getSubmittedOnDate());
}
@@ -86,6 +90,12 @@ public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
public int compareTo(@NotNull ChargeOrTransaction o) {
int datePortion = this.getEffectiveDate().compareTo(o.getEffectiveDate());
if (datePortion == 0) {
if (this.isAccrualActivity() && !o.isAccrualActivity()) {
return 1;
}
if (!this.isAccrualActivity() && o.isAccrualActivity()) {
return -1;
}
int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
if (submittedDate == 0) {
return this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
@@ -264,7 +264,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
// [For Adv payment allocation strategy] check if charge due date is earlier than last transaction
// date, if yes trigger reprocess else no reprocessing
if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(loan.transactionProcessingStrategy())) {
LoanTransaction lastPaymentTransaction = loan.getLastPaymentTransaction();
LoanTransaction lastPaymentTransaction = loan.getLastTransactionForReprocessing();
if (lastPaymentTransaction != null) {
if (loanCharge.getEffectiveDueDate() != null
&& DateUtils.isAfter(loanCharge.getEffectiveDueDate(), lastPaymentTransaction.getTransactionDate())) {
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
import static org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder.DEFAULT_STRATEGY;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -27,9 +28,14 @@ import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.fineract.client.models.AdvancedPaymentData;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostChargesRequest;
import org.apache.fineract.client.models.PostChargesResponse;
import org.apache.fineract.client.models.PostClientsResponse;
@@ -48,9 +54,13 @@ import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -92,8 +102,13 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
// charge fee with due date as 1st installment
// charge penalty with due date as 3rd installment
// charge fee with due date as 2nd installment
// set business day to the day before closing day of 1st installment, run COB for loan, verify no Accrual Activity
// posted
// set business day to the closing day of 1st installment, run COB for loan, verify Accrual Activity posted
// set business day to the day after closing day of 1st installment, run COB for loan, verify no Accrual Activity
// posted
@Test
public void test() {
public void testAccrualActivityPosting() {
final String disbursementDay = "01 January 2023";
final String repaymentPeriod1DueDate = "01 February 2023";
final String repaymentPeriod1CloseDate = "02 February 2023";
@@ -146,13 +161,250 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(19.35, "Accrual", "31 January 2023", 0, 0, 19.35, 0, 0, 0.0, 0.0),
transaction(70.65, "Accrual", "01 February 2023", 0, 0, 0.65, 40, 30, 0.0, 0.0),
transaction(90.0, "Accrual Activity", "01 February 2023", 1000, 0, 20.0, 40.0, 30.0, 0.0, 0.0),
transaction(90.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 40.0, 30.0, 0.0, 0.0),
transaction(0.71, "Accrual", "02 February 2023", 0, 0, 0.71, 0, 0, 0.0, 0.0));
});
}
// Create Loan with Interest and enabled Accrual Activity Posting
// Approve and disburse loan
// make partial repayment before first installment day
// run COB on closing day of first installment
// verify that the Accrual Activity transaction is created
// make repayment before the first repayment
// verify that Accrual Activity transaction is NOT modified/reversed/replayed
// charge backdated penalty before first installment due date
// verify that the Accrual Activity transaction is reverse replayed
// verify that the Accrual Activity holds the correct portions
// charge backdated penalty before first installment due date
// verify that the Accrual Activity transaction is reverse replayed
// verify that the Accrual Activity holds the correct portions
@Test
public void testAccrualActivityPostingReverseReplay() {
final String disbursementDay = "01 January 2023";
final String repaymentPeriod1CloseDate = "02 February 2023";
final String repaymentPeriod1OneDayAfterCloseDate = "03 February 2023";
final String creationBusinessDay = "15 May 2023";
AtomicReference<Long> loanId = new AtomicReference<>();
runAt(creationBusinessDay, () -> {
Long localLoanProductId = createLoanProductAccountingAccrualPeriodicWithInterest();
loanId.set(applyForLoanApplicationWithInterest(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000),
disbursementDay));
loanTransactionHelper.approveLoan(loanId.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000))
.dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
addRepaymentForLoan(loanId.get(), 50.0, "10 January 2023");
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 970, 30, 20, 0, 0, 0.0, 0.0));
});
runAt(repaymentPeriod1CloseDate, () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 20, 0, 0, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 970, 30, 20, 0, 0, 0.0, 0.0),
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 0.0, 0.0, 0.0, 0.0));
});
runAt(repaymentPeriod1OneDayAfterCloseDate, () -> {
addRepaymentForLoan(loanId.get(), 200.0, "8 January 2023");
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 20, 0, 0, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 770, 50, 0, 0, 0, 0.0, 0.0),
transaction(200.0, "Repayment", "08 January 2023", 820, 180, 20, 0, 0, 0.0, 0.0),
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 0.0, 0.0, 0.0, 0.0));
chargePenalty(loanId.get(), 33.0, "01 February 2023");
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 20, 0, 0, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 803, 50, 0, 0, 0, 0.0, 0.0),
transaction(200.0, "Repayment", "08 January 2023", 853, 147, 20, 0, 33, 0.0, 0.0),
transaction(53.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 0.0, 33.0, 0.0, 0.0));
chargeFee(loanId.get(), 12.0, "01 February 2023");
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 20, 0, 0, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 815, 50, 0, 0, 0, 0.0, 0.0),
transaction(200.0, "Repayment", "08 January 2023", 865, 135, 20, 12, 33, 0.0, 0.0),
transaction(65.0, "Accrual Activity", "01 February 2023", 0, 0, 20.0, 12.0, 33.0, 0.0, 0.0));
});
}
// Create Loan with Advanced Payment Allocation and enabled Accrual Activity Posting
// Approve and disburse loan
// charge penalty for 1st installment
// make partial repayment before first installment day
// run COB on closing day of first installment
// verify that the Accrual Activity transaction is created
// make repayment before the first repayment
// verify that Accrual Activity transaction is NOT modified/reversed/replayed
// charge backdated penalty before first installment due date
// verify that the Accrual Activity transaction is reverse replayed
// verify that the Accrual Activity holds the correct portions
// charge backdated penalty before first installment due date
// verify that the Accrual Activity transaction is reverse replayed
// verify that the Accrual Activity holds the correct portions
@ParameterizedTest
@CsvSource({ "29 January 2023,30 January 2023,31 January 2023", "31 January 2023,30 January 2023,29 January 2023",
"31 January 2023,31 January 2023,31 January 2023", "01 February 2023,01 February 2023,01 February 2023" })
public void testAccrualActivityPostingReverseReplayAdvancedPaymentAllocation(final String chargeDueDate1st,
final String chargeDueDate2st, final String chargeDueDate3st) {
final String disbursementDay = "01 January 2023";
final String repaymentPeriod1CloseDate = "02 February 2023";
final String repaymentPeriod1OneDayAfterCloseDate = "03 February 2023";
final String creationBusinessDay = "15 May 2023";
AtomicReference<Long> loanId = new AtomicReference<>();
runAt(creationBusinessDay, () -> {
Long localLoanProductId = createLoanProductAccountingAccrualPeriodicAdvancedPaymentAllocation();
loanId.set(applyForLoanApplicationAdvancedPaymentAllocation(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000),
disbursementDay));
loanTransactionHelper.approveLoan(loanId.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000))
.dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
LOG.info("Test Loan Product Id {1} http://localhost:4200/#/products/loan-products/{}/general", localLoanProductId);
LOG.info("Test Client Id {1} http://localhost:4200/#/clients/{}", client.getClientId());
LOG.info("Test Loan Id {2} http://localhost:4200/#/clients/{}/loans-accounts/{}/transactions", client.getClientId(), loanId);
chargePenalty(loanId.get(), 20.0, chargeDueDate1st);
addRepaymentForLoan(loanId.get(), 50.0, "10 January 2023");
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 970, 30, 0, 0, 20, 0.0, 0.0));
});
runAt(repaymentPeriod1CloseDate, () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 950, 50, 0, 0, 0, 0.0, 0.0),
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
});
runAt(repaymentPeriod1OneDayAfterCloseDate, () -> {
addRepaymentForLoan(loanId.get(), 220.0, "8 January 2023");
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0.0, 0.0),
transaction(220.0, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0.0, 0.0),
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
chargePenalty(loanId.get(), 33.0, chargeDueDate2st);
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0.0, 0.0),
transaction(220.0, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0.0, 0.0),
transaction(53.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 53.0, 0.0, 0.0));
chargeFee(loanId.get(), 12.0, chargeDueDate3st);
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(50.0, "Repayment", "10 January 2023", 730, 50, 0, 0, 0, 0.0, 0.0),
transaction(220.0, "Repayment", "08 January 2023", 780, 220, 0, 0, 0, 0.0, 0.0),
transaction(65.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 12.0, 53.0, 0.0, 0.0));
});
}
// Create Loan with Advanced Payment Allocation and enabled Accrual Activity Posting
// Approve and disburse loan
// charge penalty for 1st installment
// run COB on closing day of first installment
// verify that the Accrual Activity transaction is created
// charge backdated fee before first installment due date
// verify that the Accrual Activity transaction is reverse replayed
// verify that the Accrual Activity holds the correct portions
@ParameterizedTest
@CsvSource({ "29 January 2023,30 January 2023,31 January 2023", "31 January 2023,30 January 2023,29 January 2023",
"31 January 2023,31 January 2023,31 January 2023", "01 February 2023,01 February 2023,01 February 2023" })
public void testAccrualActivityPostingReverseReplayAdvancedPaymentAllocationBasicFlow(final String chargeDueDate1st,
final String chargeDueDate2st, final String chargeDueDate3st) {
final String disbursementDay = "01 January 2023";
final String repaymentPeriod1CloseDate = "02 February 2023";
final String repaymentPeriod1OneDayAfterCloseDate = "03 February 2023";
final String creationBusinessDay = "15 May 2023";
AtomicReference<Long> loanId = new AtomicReference<>();
runAt(creationBusinessDay, () -> {
Long localLoanProductId = createLoanProductAccountingAccrualPeriodicAdvancedPaymentAllocation();
loanId.set(applyForLoanApplicationAdvancedPaymentAllocation(client.getClientId(), localLoanProductId, BigDecimal.valueOf(40000),
disbursementDay));
loanTransactionHelper.approveLoan(loanId.get(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000))
.dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
LOG.info("Test Loan Product Id {1} http://localhost:4200/#/products/loan-products/{}/general", localLoanProductId);
LOG.info("Test Client Id {1} http://localhost:4200/#/clients/{}", client.getClientId());
LOG.info("Test Loan Id {2} http://localhost:4200/#/clients/{}/loans-accounts/{}/transactions", client.getClientId(), loanId);
chargePenalty(loanId.get(), 20.0, chargeDueDate1st);
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0));
});
runAt(repaymentPeriod1CloseDate, () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(20.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
});
runAt(repaymentPeriod1OneDayAfterCloseDate, () -> {
chargePenalty(loanId.get(), 33.0, chargeDueDate2st);
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(53.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 0.0, 53.0, 0.0, 0.0));
chargeFee(loanId.get(), 12.0, chargeDueDate3st);
verifyTransactions(loanId.get(), //
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0, 0, 20, 0.0, 0.0),
transaction(65.0, "Accrual Activity", "01 February 2023", 0, 0, 0.0, 12.0, 53.0, 0.0, 0.0));
});
}
private void chargeFee(Long loanId, Double amount, String dueDate) {
LOG.info("----------------------------------CHARGE FEE---------------------------------");
LOG.info("FEE amount {} dueDate {}", amount, dueDate);
PostChargesResponse feeCharge = chargesHelper.createCharges(new PostChargesRequest().penalty(false).amount(9.0)
.chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue())
.chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD")
@@ -166,6 +418,8 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
}
private void chargePenalty(Long loanId, Double amount, String dueDate) {
LOG.info("----------------------------------CHARGE PENALTY---------------------------------");
LOG.info("PENALTY amount {} dueDate {}", amount, dueDate);
PostChargesResponse penaltyCharge = chargesHelper.createCharges(new PostChargesRequest().penalty(true).amount(10.0)
.chargeCalculationType(ChargeCalculationType.FLAT.getValue()).chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue())
.chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD")
@@ -176,6 +430,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
.amount(amount).dueDate(dueDate));
assertNotNull(penaltyLoanChargeResult);
assertNotNull(penaltyLoanChargeResult.getResourceId());
LOG.info("----------------------------------CHARGE PENALTY---------------------------------");
}
private Long createLoanProductAccountingAccrualPeriodicWithInterest() {
@@ -225,4 +480,79 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
return loanTransactionHelper.applyLoan(loanRequest).getLoanId();
}
private Long createLoanProductAccountingAccrualPeriodicAdvancedPaymentAllocation() {
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
String shortName = Utils.uniqueRandomStringGenerator("", 4);
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
return loanTransactionHelper.createLoanProduct(new PostLoanProductsRequest().name(name).shortName(shortName)
.description("Test loan description").currencyCode("USD").digitsAfterDecimal(2).daysInYearType(1).daysInMonthType(1)
.recalculationRestFrequencyType(1).rescheduleStrategyMethod(1).loanScheduleType(LoanScheduleType.PROGRESSIVE.name())
.recalculationRestFrequencyInterval(0).isInterestRecalculationEnabled(false).locale("en_GB").numberOfRepayments(4)
.repaymentFrequencyType(2L).repaymentEvery(1).minPrincipal(100.0).principal(1000.0).maxPrincipal(10000000.0)
.amortizationType(1).interestType(0).interestRatePerPeriod(0.0).interestRateFrequencyType(1)
.interestCalculationPeriodType(1).dateFormat("dd MMMM yyyy")
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).paymentAllocation(List.of(defaultAllocation))
.accountingRule(3).enableAccrualActivityPosting(true).fundSourceAccountId(fundSource.getAccountID().longValue())//
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
.incomeFromPenaltyAccountId(feeIncomeAccount.getAccountID().longValue())//
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
.receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())//
.receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())//
.receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())//
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
.incomeFromGoodwillCreditInterestAccountId(goodwillIncomeAccount.getAccountID().longValue())//
.incomeFromGoodwillCreditFeesAccountId(goodwillIncomeAccount.getAccountID().longValue())//
.incomeFromGoodwillCreditPenaltyAccountId(goodwillIncomeAccount.getAccountID().longValue())//
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
).getResourceId();
}
private static List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
AtomicInteger integer = new AtomicInteger(1);
return Arrays.stream(paymentAllocationTypes).map(pat -> {
PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
paymentAllocationOrder.setOrder(integer.getAndIncrement());
return paymentAllocationOrder;
}).collect(Collectors.toList());
}
private static AdvancedPaymentData createDefaultPaymentAllocation() {
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
advancedPaymentData.setTransactionType("DEFAULT");
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
return advancedPaymentData;
}
private static Long applyForLoanApplicationAdvancedPaymentAllocation(final Long clientID, final Long loanProductID,
BigDecimal principal, String applicationDisbursementDate) {
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
final PostLoansRequest loanRequest = new PostLoansRequest() //
.loanTermFrequency(4).locale("en_GB").loanTermFrequencyType(2).numberOfRepayments(4).repaymentFrequencyType(2)
.repaymentEvery(1).principal(principal).amortizationType(1).interestType(0).interestRatePerPeriod(BigDecimal.ZERO)
.interestCalculationPeriodType(1).dateFormat("dd MMMM yyyy")
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).loanType("individual")
.expectedDisbursementDate(applicationDisbursementDate).submittedOnDate(applicationDisbursementDate).clientId(clientID)
.productId(loanProductID);
return loanTransactionHelper.applyLoan(loanRequest).getLoanId();
}
}