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 78cd5b106 FINERACT-2081: Fix Accrual Activity Reversal on Installment 
Due Date. * Accrual Activity is not recalculated during reopen transaction 
recalculation * integration tests cover reverse replay verification and extra 
use cases
78cd5b106 is described below

commit 78cd5b106abb940893da26d54519fa2c54069390
Author: Soma Sörös <[email protected]>
AuthorDate: Wed Jan 29 10:44:13 2025 +0100

    FINERACT-2081: Fix Accrual Activity Reversal on Installment Due Date.
    * Accrual Activity is not recalculated during reopen transaction 
recalculation
    * integration tests cover reverse replay verification and extra use cases
---
 ...tLoanRepaymentScheduleTransactionProcessor.java |  13 +-
 .../integrationtests/BaseLoanIntegrationTest.java  |  28 ++
 .../ExternalBusinessEventTest.java                 |  78 +++++
 .../LoanTransactionAccrualActivityPostingTest.java | 343 +++++++++++++++++++--
 .../common/loans/LoanTransactionHelper.java        |  13 +-
 5 files changed, 450 insertions(+), 25 deletions(-)

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 bae506da0..c6c0da186 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
@@ -185,7 +185,10 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
                      * Check if the transaction amounts have changed. If so, 
reverse the original transaction and update
                      * changedTransactionDetail accordingly
                      **/
-                    if (LoanTransaction.transactionAmountsMatch(currency, 
loanTransaction, newLoanTransaction)) {
+                    if (newLoanTransaction.isReversed()) {
+                        loanTransaction.reverse();
+                        
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(),
 loanTransaction);
+                    } else if 
(LoanTransaction.transactionAmountsMatch(currency, loanTransaction, 
newLoanTransaction)) {
                         
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
                                 
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
                     } else {
@@ -225,8 +228,12 @@ public abstract class 
AbstractLoanRepaymentScheduleTransactionProcessor implemen
                 .filter(installment -> 
LoanRepaymentScheduleProcessingWrapper.isInPeriod(loanTransaction.getTransactionDate(),
 installment,
                         
installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)))
                 .findFirst().orElseThrow();
-        if 
(loanTransaction.getDateOf().isEqual(currentInstallment.getDueDate()) || 
installments.stream()
-                .filter(i -> !i.isAdditional() && 
!i.isDownPayment()).noneMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff))
 {
+
+        if (currentInstallment.isNotFullyPaidOff() && 
(currentInstallment.getDueDate().isAfter(loanTransaction.getTransactionDate())
+                || 
(currentInstallment.getDueDate().isEqual(loanTransaction.getTransactionDate())
+                        && 
loanTransaction.getTransactionDate().equals(DateUtils.getBusinessLocalDate()))))
 {
+            loanTransaction.reverse();
+        } else {
             loanTransaction.resetDerivedComponents();
             final Money principalPortion = Money.zero(currency);
             Money interestPortion = 
currentInstallment.getInterestCharged(currency);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index 2ed80f7cf..b484f8903 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -50,6 +50,7 @@ import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import lombok.AllArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.ToString;
@@ -62,6 +63,7 @@ import org.apache.fineract.client.models.BusinessDateRequest;
 import 
org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdStatus;
 import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
 import org.apache.fineract.client.models.JournalEntryTransactionItem;
 import org.apache.fineract.client.models.PaymentAllocationOrder;
@@ -238,6 +240,32 @@ public abstract class BaseLoanIntegrationTest {
         assertEquals(paidLate, period.getTotalPaidLateForPeriod());
     }
 
+    /**
+     * Verifies the loan status by applying the given extractor function to 
the status of the loan details. This method
+     * ensures that the loan details, loan status, and the result of the 
extractor are not null and asserts that the
+     * result of the extractor function is true.
+     *
+     * @param loanDetails
+     *            the loan details object containing the loan status
+     * @param extractor
+     *            a function that extracts a boolean value from the loan 
status for verification
+     * @throws AssertionError
+     *             if any of the following conditions are not met:
+     *             <ul>
+     *             <li>The loan details object is not null</li>
+     *             <li>The loan status in the loan details is not null</li>
+     *             <li>The value extracted by the extractor function is not 
null</li>
+     *             <li>The value extracted by the extractor function is 
true</li>
+     *             </ul>
+     */
+    protected void verifyLoanStatus(GetLoansLoanIdResponse loanDetails, 
Function<GetLoansLoanIdStatus, Boolean> extractor) {
+        Assertions.assertNotNull(loanDetails);
+        Assertions.assertNotNull(loanDetails.getStatus());
+        Boolean actualValue = extractor.apply(loanDetails.getStatus());
+        Assertions.assertNotNull(actualValue);
+        Assertions.assertTrue(actualValue);
+    }
+
     private String getNonByPassUserAuthKey(RequestSpecification requestSpec, 
ResponseSpecification responseSpec) {
         // creates the user
         UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, 
responseSpec);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
index 321e46256..3b2e7dbea 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
@@ -42,6 +42,8 @@ import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdStatus;
 import org.apache.fineract.client.models.GlobalConfigurationPropertyData;
 import org.apache.fineract.client.models.PostClientsResponse;
 import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
@@ -53,6 +55,7 @@ import 
org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
 import org.apache.fineract.client.models.PostLoansLoanIdRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
 import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
 import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
 import 
org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
@@ -715,6 +718,81 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
         });
     }
 
+    /**
+     * Using Interest bearing Progressive Loan, Accrual Activity Posting, 
InterestRecalculation, 25% yearly interest 6
+     * repayment 450 USD principal.
+     * <li>apply, approve and disburse backdated on 17 August 2024</li>
+     * <li>repay 600 on 17 January 2025</li>
+     * <li>verify Accrual and Accrual Activity transaction creation</li>
+     * <li>verify that the loan become overpaid</li>
+     * <li>reverse repayment on same day</li>
+     * <li>verify there is no reverse replayed transaction during reversing 
the repayment</li>
+     * <li>verify transaction reversals</li>
+     */
+    @Test
+    public void 
testInterestBearingProgressiveInterestRecalculationReopenDueReverseRepayment() {
+        runAt("17 January 2025", () -> {
+            
externalEventHelper.enableBusinessEvent("LoanAdjustTransactionBusinessEvent");
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, NO InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(false));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 600.0f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(600.0, "Repayment", "17 January 2025"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(4.99, "Accrual Activity", "17 January 2025")); 
//
+            deleteAllExternalEvents();
+            loanTransactionHelper.reverseRepayment(loanId.intValue(), 
repaymentId.intValue(), "17 January 2025");
+
+            List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            // Verify that there were no reverse-replay event
+            List<ExternalEventDTO> list = allExternalEvents.stream() //
+                    .filter(x -> 
"LoanAdjustTransactionBusinessEvent".equals(x.getType()) //
+                            && x.getPayLoad().get("newTransactionDetail") != 
null //
+                            && x.getPayLoad().get("transactionToAdjust") != 
null) //
+                    .toList(); //
+            Assertions.assertEquals(0, list.size());
+
+            // verify that there were 2 transaction reversal event
+            list = allExternalEvents.stream() //
+                    .filter(x -> 
"LoanAdjustTransactionBusinessEvent".equals(x.getType()) //
+                            && x.getPayLoad().get("newTransactionDetail") == 
null //
+                            && x.getPayLoad().get("transactionToAdjust") != 
null) //
+                    .toList(); //
+            Assertions.assertEquals(2, list.size());
+
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    reversedTransaction(600.0, "Repayment", "17 January 
2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 
2024")); //
+        });
+    }
+
     @Test
     public void 
verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundWithInterestRefund()
 {
         AtomicReference<Long> loanIdRef = new AtomicReference<>();
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 e5c9e25d8..d50294e3c 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
@@ -43,6 +43,7 @@ import 
org.apache.fineract.client.models.ChargeToGLAccountMapper;
 import org.apache.fineract.client.models.GetLoanFeeToIncomeAccountMappings;
 import 
org.apache.fineract.client.models.GetLoanPaymentChannelToFundSourceMappings;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdStatus;
 import org.apache.fineract.client.models.PaymentAllocationOrder;
 import org.apache.fineract.client.models.PostChargesRequest;
 import org.apache.fineract.client.models.PostChargesResponse;
@@ -108,6 +109,323 @@ public class LoanTransactionAccrualActivityPostingTest 
extends BaseLoanIntegrati
                 "EXTERNAL_ASSET_OWNER_TRANSFER", "ACCRUAL_ACTIVITY_POSTING");
     }
 
+    /**
+     * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO 
InterestRecalculation, 25% yearly interest
+     * 6 repayment 450 USD principal.
+     * <li>apply, approve and disburse backdated on 17 August 2024</li>
+     * <li>repay 600 on 17 January 2025</li>
+     * <li>verify Accrual and Accrual Activity transaction creation</li>
+     * <li>verify that the loan become overpaid</li>
+     * <li>reverse repayment on same day</li>
+     * <li>verify transaction reversals</li>
+     */
+    @Test
+    public void 
testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment1()
 {
+        runAt("17 January 2025", () -> {
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, NO InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(false));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 600.0f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(600.0, "Repayment", "17 January 2025"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(4.99, "Accrual Activity", "17 January 2025")); 
//
+            loanTransactionHelper.reverseRepayment(loanId.intValue(), 
repaymentId.intValue(), "17 January 2025");
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    reversedTransaction(600.0, "Repayment", "17 January 
2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 
2024")); //
+        });
+    }
+
+    /**
+     * Using Interest bearing Progressive Loan, Accrual Activity Posting, 
InterestRecalculation, 25% yearly interest 6
+     * repayment 450 USD principal.
+     * <li>apply, approve and disburse backdated on 17 August 2024</li>
+     * <li>repay 600 on 17 January 2025</li>
+     * <li>verify Accrual and Accrual Activity transaction creation</li>
+     * <li>verify that the loan become overpaid</li>
+     * <li>reverse repayment on same day</li>
+     * <li>verify transaction reversals</li>
+     */
+    @Test
+    public void 
testInterestBearingProgressiveInterestRecalculationReopenDueReverseRepayment() {
+        runAt("17 January 2025", () -> {
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, NO InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(false));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 600.0f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(600.0, "Repayment", "17 January 2025"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(4.99, "Accrual Activity", "17 January 2025")); 
//
+            loanTransactionHelper.reverseRepayment(loanId.intValue(), 
repaymentId.intValue(), "17 January 2025");
+
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    reversedTransaction(600.0, "Repayment", "17 January 
2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 
2024")); //
+        });
+    }
+
+    /**
+     * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO 
InterestRecalculation, 25% yearly interest
+     * 6 repayment 450 USD principal.
+     * <li>apply, approve and disburse backdated on 17 August 2024</li>
+     * <li>repay 600 on 17 January 2025</li>
+     * <li>verify Accrual and Accrual Activity transaction creation</li>
+     * <li>verify that the loan become overpaid</li>
+     * <li>reverse repayment on same day</li>
+     * <li>verify transaction reversals</li>
+     */
+    @Test
+    public void 
testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment1b()
 {
+        runAt("18 January 2025", () -> {
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, NO InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(false));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 600.0f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(600.0, "Repayment", "17 January 2025"), //
+                    transaction(33.52, "Accrual", "18 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(4.99, "Accrual Activity", "17 January 2025")); 
//
+            loanTransactionHelper.reverseRepayment(loanId.intValue(), 
repaymentId.intValue(), "17 January 2025");
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(33.52, "Accrual", "18 January 2025"), //
+                    reversedTransaction(600.0, "Repayment", "17 January 
2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(3.31, "Accrual Activity", "17 January 2025")); 
//
+        });
+    }
+
+    @Test
+    public void 
testAccrualActivityPostingAndReversalsInterestBearingProgressiveInterestRecalculationMerchantIssuedRefund()
 {
+        runAt("17 January 2025", () -> {
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .currencyCode("USD") //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(true));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 497.04f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, 
GetLoansLoanIdStatus::getClosedObligationsMet);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(497.04, "Repayment", "17 January 2025"), //
+                    transaction(47.04, "Accrual", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(9.22, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(9.53, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(9.22, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(9.54, "Accrual Activity", "17 January 2025")); 
//
+            loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", 
"17 August 2024", 450.0f, loanId.intValue()).getResourceId();
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(450.0, "Merchant Issued Refund", "17 August 
2024"), //
+                    transaction(497.04, "Repayment", "17 January 2025"), //
+                    transaction(47.04, "Accrual", "17 January 2025"), //
+                    transaction(47.04, "Accrual Adjustment", "17 January 
2025")); //
+        });
+    }
+
+    /**
+     * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO 
InterestRecalculation, 25% yearly interest
+     * 6 repayment 450 USD principal.
+     * <li>apply, approve and disburse backdated on 17 August 2024</li>
+     * <li>repay 600 on 17 January 2025</li>
+     * <li>verify Accrual and Accrual Activity transaction creation</li>
+     * <li>verify that the loan become overpaid</li>
+     * <li>reverse repayment on same day verify transaction reversals</li>
+     */
+    @Test
+    public void 
testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment2b()
 {
+        runAt("17 January 2025", () -> {
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, NO InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .currencyCode("USD") //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(false));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 483.52f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, 
GetLoansLoanIdStatus::getClosedObligationsMet);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(483.52, "Repayment", "17 January 2025"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(4.99, "Accrual Activity", "17 January 2025")); 
//
+            addCharge(loanId, false, 15.0, "15 January 2025");
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(33.52, "Accrual", "17 January 2025"), //
+                    transaction(483.52, "Repayment", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 
2024")); //
+        });
+    }
+
+    /**
+     * Using Interest bearing Progressive Loan, Accrual Activity Posting, NO 
InterestRecalculation, 25% yearly interest
+     * 6 repayment 450 USD principal.
+     * <li>apply, approve and disburse backdated on 17 August 2024</li>
+     * <li>repay 600 on 17 January 2025</li>
+     * <li>verify Accrual and Accrual Activity transaction creation</li>
+     * <li>verify that the loan become overpaid</li>
+     * <li>reverse repayment on same day verify transaction reversals</li>
+     */
+    @Test
+    public void 
testInterestBearingProgressiveNoInterestRecalculationReopenDueReverseRepayment2c()
 {
+        runAt("18 January 2025", () -> {
+            final PostLoanProductsResponse loanProductsResponse = 
loanProductHelper.createLoanProduct(create4IProgressive() //
+                    .description("Interest bearing Progressive Loan USD, 
Accrual Activity Posting, NO InterestRecalculation") //
+                    .enableAccrualActivityPosting(true) //
+                    .currencyCode("USD") //
+                    .daysInMonthType(DaysInMonthType.ACTUAL) //
+                    .daysInYearType(DaysInYearType.ACTUAL) //
+                    .isInterestRecalculationEnabled(false));//
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(),
+                    loanProductsResponse.getResourceId(), "17 August 2024", 
450.0, 25.0, 6, null));
+            Long loanId = postLoansResponse.getLoanId();
+            Assertions.assertNotNull(loanId);
+            loanTransactionHelper.approveLoan(loanId, 
approveLoanRequest(450.0, "17 August 2024"));
+            disburseLoan(loanId, BigDecimal.valueOf(450.0), "17 August 2024");
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024") //
+            );
+            Long repaymentId = loanTransactionHelper.makeLoanRepayment("17 
January 2025", 483.52f, loanId.intValue()).getResourceId();
+            Assertions.assertNotNull(repaymentId);
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, 
GetLoansLoanIdStatus::getClosedObligationsMet);
+            verifyTransactions(loanId, //
+                    transaction(450.0, "Disbursement", "17 August 2024"), //
+                    transaction(483.52, "Repayment", "17 January 2025"), //
+                    transaction(33.52, "Accrual", "18 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(4.99, "Accrual Activity", "17 January 2025")); 
//
+            addCharge(loanId, false, 15.0, "15 January 2025");
+            loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
+            verifyTransactions(loanId, transaction(450.0, "Disbursement", "17 
August 2024"), //
+                    transaction(33.52, "Accrual", "18 January 2025"), //
+                    transaction(483.52, "Repayment", "17 January 2025"), //
+                    transaction(9.53, "Accrual Activity", "17 September 
2024"), //
+                    transaction(7.77, "Accrual Activity", "17 October 2024"), 
//
+                    transaction(6.48, "Accrual Activity", "17 November 2024"), 
//
+                    transaction(4.75, "Accrual Activity", "17 December 2024"), 
//
+                    transaction(18.31, "Accrual Activity", "17 January 
2025")); //
+
+        });
+    }
+
     // Create Loan with Interest and enabled Accrual Activity Posting
     // Approve and disburse loan
     // charge penalty with due date as 1st installment
@@ -955,10 +1273,7 @@ public class LoanTransactionAccrualActivityPostingTest 
extends BaseLoanIntegrati
             Long repaymentId = loanTransactionHelper.makeLoanRepayment("02 
January 2024", 370.0f, loanId.intValue()).getResourceId();
             Assertions.assertNotNull(repaymentId);
             GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
-            Assertions.assertNotNull(loanDetails);
-            Assertions.assertNotNull(loanDetails.getStatus());
-            Assertions.assertNotNull(loanDetails.getStatus().getOverpaid());
-            Assertions.assertTrue(loanDetails.getStatus().getOverpaid());
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
 
             verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 
January 2024"),
                     transaction(100.0, "Down Payment", "01 January 2024"), 
transaction(8.76, "Accrual", "02 January 2024"),
@@ -991,10 +1306,7 @@ public class LoanTransactionAccrualActivityPostingTest 
extends BaseLoanIntegrati
             Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 
January 2024", 370.0f, loanId.intValue()).getResourceId();
             Assertions.assertNotNull(repaymentId);
             GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
-            Assertions.assertNotNull(loanDetails);
-            Assertions.assertNotNull(loanDetails.getStatus());
-            Assertions.assertNotNull(loanDetails.getStatus().getOverpaid());
-            Assertions.assertTrue(loanDetails.getStatus().getOverpaid());
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
 
             verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 
January 2024"),
                     transaction(100.0, "Down Payment", "01 January 2024"), 
transaction(8.76, "Accrual", "01 January 2024"),
@@ -1002,10 +1314,7 @@ public class LoanTransactionAccrualActivityPostingTest 
extends BaseLoanIntegrati
 
             loanTransactionHelper.reverseRepayment(loanId.intValue(), 
repaymentId.intValue(), "01 January 2024");
             loanDetails = loanTransactionHelper.getLoanDetails(loanId);
-            Assertions.assertNotNull(loanDetails);
-            Assertions.assertNotNull(loanDetails.getStatus());
-            Assertions.assertNotNull(loanDetails.getStatus().getActive());
-            Assertions.assertTrue(loanDetails.getStatus().getActive());
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
             verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 
January 2024"),
                     transaction(100.0, "Down Payment", "01 January 2024"), 
transaction(8.76, "Accrual", "01 January 2024"),
                     reversedTransaction(370.0, "Repayment", "01 January 
2024"));
@@ -1038,20 +1347,14 @@ public class LoanTransactionAccrualActivityPostingTest 
extends BaseLoanIntegrati
             Long repaymentId = loanTransactionHelper.makeLoanRepayment("01 
January 2024", 370.0f, loanId.intValue()).getResourceId();
             Assertions.assertNotNull(repaymentId);
             GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
-            Assertions.assertNotNull(loanDetails);
-            Assertions.assertNotNull(loanDetails.getStatus());
-            Assertions.assertNotNull(loanDetails.getStatus().getOverpaid());
-            Assertions.assertTrue(loanDetails.getStatus().getOverpaid());
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getOverpaid);
 
             verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 
January 2024"),
                     transaction(100.0, "Down Payment", "01 January 2024"), 
transaction(38.76, "Accrual", "01 January 2024"),
                     transaction(38.76, "Accrual Activity", "01 January 2024"), 
transaction(370.0, "Repayment", "01 January 2024"));
             loanTransactionHelper.reverseRepayment(loanId.intValue(), 
repaymentId.intValue(), "01 January 2024");
             loanDetails = loanTransactionHelper.getLoanDetails(loanId);
-            Assertions.assertNotNull(loanDetails);
-            Assertions.assertNotNull(loanDetails.getStatus());
-            Assertions.assertNotNull(loanDetails.getStatus().getActive());
-            Assertions.assertTrue(loanDetails.getStatus().getActive());
+            verifyLoanStatus(loanDetails, GetLoansLoanIdStatus::getActive);
             verifyTransactions(loanId, transaction(400.0, "Disbursement", "01 
January 2024"),
                     transaction(100.0, "Down Payment", "01 January 2024"), 
transaction(38.76, "Accrual", "01 January 2024"),
                     reversedTransaction(370.0, "Repayment", "01 January 
2024"));
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 81a51b314..c030ed20e 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -100,6 +100,7 @@ import org.apache.poi.ss.usermodel.Workbook;
 @SuppressWarnings({ "rawtypes", "unchecked" })
 public class LoanTransactionHelper extends IntegrationTest {
 
+    public static final String DATE_FORMAT = "d MMMM yyyy";
     public static final String DATE_TIME_FORMAT = "dd MMMM yyyy HH:mm";
     private static final String LOAN_PRODUCTS_URL = 
"/fineract-provider/api/v1/loanproducts";
     private static final String CREATE_LOAN_PRODUCT_URL = 
"/fineract-provider/api/v1/loanproducts?" + Utils.TENANT_IDENTIFIER;
@@ -871,9 +872,10 @@ public class LoanTransactionHelper extends IntegrationTest 
{
     }
 
     public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long 
loanId, final String command, final String date,
-            final Double amountToBePaid) {
+            final Double amount) {
+        log.info("Make {} with amount {} in {} for Loan {}", command, amount, 
date, loanId);
         return ok(fineract().loanTransactions.executeLoanTransaction(loanId, 
new PostLoansLoanIdTransactionsRequest()
-                
.transactionAmount(amountToBePaid).transactionDate(date).dateFormat("dd MMMM 
yyyy").locale("en"), command));
+                
.transactionAmount(amount).transactionDate(date).dateFormat("dd MMMM 
yyyy").locale("en"), command));
     }
 
     // TODO: Rewrite to use fineract-client instead!
@@ -1210,6 +1212,13 @@ public class LoanTransactionHelper extends 
IntegrationTest {
         return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, 
transactionId, request, "undo"));
     }
 
+    public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final 
Long loanId, final Long transactionId, String date) {
+        return ok(fineract().loanTransactions.adjustLoanTransaction(loanId, 
transactionId,
+                new 
PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATE_FORMAT).transactionDate(date).transactionAmount(0.0)
+                        .locale("en"),
+                "undo"));
+    }
+
     public HashMap makeRepaymentWithPDC(final String date, final Float 
amountToBePaid, final Integer loanID, final Long paymentType) {
         return (HashMap) 
performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID),
                 getRepaymentWithPDCBodyAsJSON(date, amountToBePaid, 
paymentType), "");


Reply via email to