From 9fceaf98b3b1ce3657413ee5feef91f54341e5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Tue, 9 Jul 2024 17:05:39 +0200 Subject: [PATCH] FINERACT-2104: Accrual Activity Reverse Replay Transaction --- .../portfolio/loanaccount/domain/Loan.java | 26 +- .../loanaccount/domain/LoanTransaction.java | 16 +- ...RepaymentScheduleTransactionProcessor.java | 31 ++ ...stractCumulativeLoanScheduleGenerator.java | 2 +- ...edPaymentScheduleTransactionProcessor.java | 1 + .../impl/ChargeOrTransaction.java | 10 + .../LoanChargeWritePlatformServiceImpl.java | 2 +- ...TransactionAccrualActivityPostingTest.java | 334 +++++++++++++++++- 8 files changed, 398 insertions(+), 24 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 448946b55..4e232000e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -2481,24 +2481,19 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } public List 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 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 loanTransactionForReprocessingPredicate() { + return transaction -> transaction.isNotReversed() && (transaction.isChargeOff() || transaction.isReAge() + || transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction()); } private List retrieveListOfTransactionsExcludeAccruals() { final List 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 { 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 { } outstanding = outstanding.plus(transactionOutstanding); loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); - } else { + } else if (!loanTransaction.isAccrualActivity()) { if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction() && !loanTransaction.isRepaymentAtDisbursement()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index eb7548fd7..26e7dfddd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -879,11 +879,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom getLoanCollateralManagementSet() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index ba9eccf37..f1151b6e9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -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 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 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()) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index b68e5d5f9..7e46ce5cd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java @@ -2785,7 +2785,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO, loanRepaymentScheduleTransactionProcessor, onDate, calculateTill); - List loanTransactions = loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); + List loanTransactions = loan.retrieveListOfTransactionsForReprocessing(); loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges()); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 7513f0dbf..0dfbfb7a8 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -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()); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java index 4e69c90c3..90cf9bd7e 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java @@ -57,6 +57,10 @@ public class ChargeOrTransaction implements Comparable { } } + 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 { 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()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 67bc2085e..4f14bd250 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -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())) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java index 1d833e8e3..6db3a3d5c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java @@ -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 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 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 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 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 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(); + } + }