This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 9fceaf98b FINERACT-2104: Accrual Activity Reverse Replay Transaction
9fceaf98b is described below
commit 9fceaf98b3b1ce3657413ee5feef91f54341e5c7
Author: Soma Sörös <[email protected]>
AuthorDate: Tue Jul 9 17:05:39 2024 +0200
FINERACT-2104: Accrual Activity Reverse Replay Transaction
---
.../portfolio/loanaccount/domain/Loan.java | 26 +-
.../loanaccount/domain/LoanTransaction.java | 16 +-
...tLoanRepaymentScheduleTransactionProcessor.java | 31 ++
.../AbstractCumulativeLoanScheduleGenerator.java | 2 +-
...dvancedPaymentScheduleTransactionProcessor.java | 1 +
.../impl/ChargeOrTransaction.java | 10 +
.../LoanChargeWritePlatformServiceImpl.java | 2 +-
.../LoanTransactionAccrualActivityPostingTest.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<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()) {
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<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() {
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<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()) {
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<LoanTransaction> loanTransactions =
loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals();
+ List<LoanTransaction> 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<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());
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<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();
+ }
+
}