This is an automated email from the ASF dual-hosted git repository.

arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit a158c930bcfb2dea2e60dd5fa2c9e91dd6feeece
Author: Arnold Galovics <[email protected]>
AuthorDate: Wed Jun 18 18:28:31 2025 +0200

    FINERACT-2181: Prepayment flow test fixes
---
 .../features/LoanAccrualTransaction.feature        |  4 +-
 .../domain/LoanAccountDomainServiceJpa.java        | 14 ++--
 .../domain/LoanAccountDomainServiceJpaHelper.java  | 83 ++++++++++++++++++++++
 .../service/LoanPointInTimeServiceImpl.java        | 13 +++-
 .../loan/pointintime/LoanPointInTimeTest.java      |  2 +-
 5 files changed, 101 insertions(+), 15 deletions(-)

diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature
 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature
index 5afc0213e8..a38acdb51a 100644
--- 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature
+++ 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature
@@ -143,11 +143,11 @@ Feature: LoanAccrualTransaction
     And Admin successfully approves the loan on "01 January 2023" with "1000" 
amount and expected disbursement date on "01 January 2023"
     When Admin successfully disburse the loan on "01 January 2023" with "1000" 
EUR transaction amount
     When Admin sets the business date to "02 January 2023"
-    And Customer makes "AUTOPAY" repayment on "02 January 2023" with 1010.19 
EUR transaction amount
+    And Customer makes "AUTOPAY" repayment on "02 January 2023" with 1000.33 
EUR transaction amount
     Then Loan status will be "CLOSED_OBLIGATIONS_MET"
     Then Loan Transactions tab has a transaction with date: "02 January 2023", 
and with the following data:
       | Transaction Type | Amount | Principal | Interest | Fees | Penalties | 
Loan Balance |
-      | Accrual          | 10.19  | 0.0       | 10.19    | 0.0  | 0.0       | 
0.0          |
+      | Accrual          | 0.33   | 0.0       | 0.33     | 0.0  | 0.0       | 
0.0          |
     Then LoanAccrualTransactionCreatedBusinessEvent is raised on "02 January 
2023"
 
   @TestRailId:C2683
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index e3e25ce8cc..08f66a0859 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -164,6 +164,7 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
     private final LoanTransactionProcessingService 
loanTransactionProcessingService;
     private final LoanBalanceService loanBalanceService;
     private final LoanTransactionService loanTransactionService;
+    private final LoanAccountDomainServiceJpaHelper 
loanAccountDomainServiceJpaHelper;
 
     @Transactional
     @Override
@@ -242,21 +243,14 @@ public class LoanAccountDomainServiceJpa implements 
LoanAccountDomainService {
         }
 
         LocalDate recalculateFrom = null;
-        if (loan.isInterestBearingAndInterestRecalculationEnabled() && 
loan.getLoanProduct().getProductInterestRecalculationDetails()
-                
.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled()) 
{
+        if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
             recalculateFrom = transactionDate;
         }
         final ScheduleGeneratorDTO scheduleGeneratorDTOForPrepay = 
this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
                 transactionDate, holidayDetailDto);
 
-        Money outstanding = 
loanTransactionProcessingService.fetchPrepaymentDetail(scheduleGeneratorDTOForPrepay,
 transactionDate, loan)
-                .getTotalOutstanding();
-        LocalDate recalculateTill = null;
-        if (repaymentAmount.isGreaterThanOrEqualTo(outstanding) && 
loan.isInterestBearingAndInterestRecalculationEnabled()
-                && 
loan.getLoanProduct().getProductInterestRecalculationDetails().getPreCloseInterestCalculationStrategy()
-                        .calculateTillPreClosureDateEnabled()) {
-            recalculateTill = transactionDate;
-        }
+        LocalDate recalculateTill = 
loanAccountDomainServiceJpaHelper.calculateRecalculateTillDate(loan, 
transactionDate,
+                scheduleGeneratorDTOForPrepay, repaymentAmount);
 
         final ScheduleGeneratorDTO scheduleGeneratorDTO = 
this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
                 recalculateTill, holidayDetailDto);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpaHelper.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpaHelper.java
new file mode 100644
index 0000000000..ff1be8aaf1
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpaHelper.java
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.domain;
+
+import java.time.LocalDate;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import 
org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
+import 
org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
+import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class LoanAccountDomainServiceJpaHelper {
+
+    private final LoanAssembler loanAssembler;
+    private final LoanTransactionProcessingService 
loanTransactionProcessingService;
+
+    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
+    public LocalDate calculateRecalculateTillDate(Loan loan, LocalDate 
transactionDate, ScheduleGeneratorDTO scheduleGeneratorDTOForPrepay,
+            Money repaymentAmount) {
+        LocalDate recalculateTill = null;
+        try {
+            if (FineractRequestContextHolder.isBatchRequest() && 
BatchRequestContextHolder.isEnclosingTransaction()) {
+                // In case of Batch requests with enclosing transaction, the 
current way of calculating the prepayment
+                // amount (since it changes
+                // the state of entities which would be written back to the 
DB) is incorrect, so we won't allow it for
+                // now.
+                // With enclosing transactions where the loan is created and 
repaid within the same batch request, due
+                // to REQUIRES_NEW, this method
+                // will simply not see that a loan has been created.
+                // Temporarily if you wanna use the batch API for prepayment, 
make sure to split the requests in a way
+                // that loan creation and
+                // repayment doesn't occur in the same batch request.
+                // Example testcase:
+                // 
org.apache.fineract.integrationtests.BatchApiTest.shouldReturnOkStatusOnSuccessfulGetDatatableEntryWithNoQueryParam
+                // TODO: this can be removed if the prepayment amount 
calculation below this is fixed in a way that it
+                // doesn't change any entity
+                // but works with DTOs
+                return null;
+            }
+            loan = loanAssembler.assembleFrom(loan.getId());
+            if (loan.isInterestBearingAndInterestRecalculationEnabled() && 
loan.getLoanProduct().getProductInterestRecalculationDetails()
+                    
.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled()) 
{
+                Money outstanding = loanTransactionProcessingService
+                        .fetchPrepaymentDetail(scheduleGeneratorDTOForPrepay, 
transactionDate, loan).getTotalOutstanding();
+                if (repaymentAmount.isGreaterThanOrEqualTo(outstanding)) {
+                    recalculateTill = transactionDate;
+                }
+            }
+        } catch (Exception e) {
+            // TODO: there's a bug where the prepayment calculation fails
+            // the test-case is 
org.apache.fineract.integrationtests.LoanTransactionAccrualActivityPostingTest.test
 in
+            // the integration-tests
+            // seems like it occurs only on CUMULATIVE loans, not PROGRESSIVE
+            log.warn("Unable to calculate prepayment amount", e);
+        }
+        return recalculateTill;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
index 7ebf62f4bd..345c8e1e79 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
@@ -18,6 +18,8 @@
  */
 package org.apache.fineract.portfolio.loanaccount.service;
 
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.FlushModeType;
 import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -35,19 +37,22 @@ import 
org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.interceptor.TransactionInterceptor;
 
 @Service
 @RequiredArgsConstructor
-@Transactional(readOnly = true)
+@Transactional
 public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
 
     private final LoanUtilService loanUtilService;
     private final LoanScheduleService loanScheduleService;
     private final LoanAssembler loanAssembler;
     private final LoanPointInTimeData.Mapper dataMapper;
+    private final EntityManager entityManager;
 
     @Override
     public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
+        entityManager.setFlushMode(FlushModeType.COMMIT);
         validateSingularRetrieval(loanId, date);
 
         // Note: since everything is running in a readOnly transaction
@@ -66,6 +71,8 @@ public class LoanPointInTimeServiceImpl implements 
LoanPointInTimeService {
 
             return dataMapper.map(loan);
         } finally {
+            entityManager.clear();
+            
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
             ThreadLocalContextUtil.setBusinessDates(originalBDs);
         }
     }
@@ -89,7 +96,9 @@ public class LoanPointInTimeServiceImpl implements 
LoanPointInTimeService {
     @Override
     public List<LoanPointInTimeData> retrieveAt(List<Long> loanIds, LocalDate 
date) {
         validateBulkRetrieval(loanIds, date);
-        return loanIds.stream().map(loanId -> retrieveAt(loanId, 
date)).toList();
+        List<LoanPointInTimeData> result = loanIds.stream().map(loanId -> 
retrieveAt(loanId, date)).toList();
+        TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
+        return result;
     }
 
     private void validateBulkRetrieval(List<Long> loanIds, LocalDate date) {
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
index 4ef88a20b0..71d42240fe 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
@@ -342,7 +342,7 @@ public class LoanPointInTimeTest extends 
BaseLoanIntegrationTest {
                     transaction(500.0, "Repayment", "09 February 2023"), //
                     transaction(500.0, "Repayment", "01 March 2023"), //
                     transaction(5032.52, "Repayment", "05 March 2023"), //
-                    transaction(1282.52, "Accrual", "05 March 2023") //
+                    transaction(1110.08, "Accrual", "05 March 2023") //
             );
         });
     }

Reply via email to