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();
+    }
+
 }


Reply via email to