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


The following commit(s) were added to refs/heads/develop by this push:
     new c9c9e9f835 FINERACT-2326: Loan point in time API now properly handles 
future dates
c9c9e9f835 is described below

commit c9c9e9f835b481663f1912653c91bde8cfbe56ed
Author: Arnold Galovics <[email protected]>
AuthorDate: Tue Oct 7 20:12:12 2025 +0200

    FINERACT-2326: Loan point in time API now properly handles future dates
---
 .../service/LoanPointInTimeServiceImpl.java        |  17 +++-
 .../integrationtests/BaseLoanIntegrationTest.java  |   2 +-
 .../loan/pointintime/LoanPointInTimeTest.java      | 112 ++++++++++++++++++++-
 3 files changed, 124 insertions(+), 7 deletions(-)

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 278c2a9a7d..da5c8db76a 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
@@ -65,11 +65,24 @@ public class LoanPointInTimeServiceImpl implements 
LoanPointInTimeService {
             ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, date)));
 
             Loan loan = loanAssembler.assembleFrom(loanId);
+
+            int txCount = loan.getLoanTransactions().size();
+            int chargeCount = loan.getCharges().size();
             removeAfterDateTransactions(loan, date);
             removeAfterDateCharges(loan, date);
+            int afterRemovalTxCount = loan.getLoanTransactions().size();
+            int afterRemovalChargeCount = loan.getCharges().size();
 
-            ScheduleGeneratorDTO scheduleGeneratorDTO = 
loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
-            loanScheduleService.recalculateSchedule(loan, 
scheduleGeneratorDTO);
+            // In case the loan is cumulative and is being prepaid by the 
latest repayment tx, we need the
+            // recalculateFrom and recalculateTill
+            // set to the same date which is the prepaying transaction's date
+            // currently this is not implemented and opens up buggy edge cases
+            // we work this around only for cases when the loan is already 
closed or the requested date doesn't change
+            // the loan's state
+            if (txCount != afterRemovalTxCount || chargeCount != 
afterRemovalChargeCount) {
+                ScheduleGeneratorDTO scheduleGeneratorDTO = 
loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
+                loanScheduleService.recalculateSchedule(loan, 
scheduleGeneratorDTO);
+            }
 
             LoanArrearsData arrearsData = 
arrearsAgingService.calculateArrearsForLoan(loan);
 
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 df8cc9d843..ba07af2d60 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
@@ -992,7 +992,7 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
         }
     }
 
-    protected void verifyArreals(LoanPointInTimeData pointInTimeData, boolean 
isOverDue, String overdueSince) {
+    protected void verifyArrears(LoanPointInTimeData pointInTimeData, boolean 
isOverDue, String overdueSince) {
         
assertThat(Objects.requireNonNull(pointInTimeData.getArrears()).getOverdue()).isEqualTo(isOverDue);
         if (isOverDue) {
             
assertThat(Objects.requireNonNull(pointInTimeData.getArrears().getOverDueSince()).toString()).isEqualTo(overdueSince);
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 969a870322..81705b52d7 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
@@ -658,7 +658,7 @@ public class LoanPointInTimeTest extends 
BaseLoanIntegrationTest {
     }
 
     @Test
-    public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
+    public void test_LoanPointInTimeDataWorks_ForArrearsDataCalculation() {
         AtomicReference<Long> aLoanId = new AtomicReference<>();
 
         runAt("01 January 2023", () -> {
@@ -683,7 +683,6 @@ public class LoanPointInTimeTest extends 
BaseLoanIntegrationTest {
                     .interestType(InterestType.DECLINING_BALANCE)//
                     
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
                     
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
-                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
                     .isInterestRecalculationEnabled(true)//
                     .recalculationRestFrequencyInterval(1)//
                     
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
@@ -737,14 +736,14 @@ public class LoanPointInTimeTest extends 
BaseLoanIntegrationTest {
 
             LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, 
"10 February 2023");
             verifyOutstanding(pointInTimeData, outstanding(5000.0, 0.0, 0.0, 
0.0, 5000.0));
-            verifyArreals(pointInTimeData, true, "2023-02-01");
+            verifyArrears(pointInTimeData, true, "2023-02-01");
 
             // repay 500
             addRepaymentForLoan(loanId, 2500.0, "01 February 2023");
 
             LoanPointInTimeData pointInTimeDataAfterRepay = 
getPointInTimeData(loanId, "10 February 2023");
             verifyOutstanding(pointInTimeDataAfterRepay, outstanding(2500.0, 
0.0, 0.0, 0.0, 2500.0));
-            verifyArreals(pointInTimeDataAfterRepay, false, null);
+            verifyArrears(pointInTimeDataAfterRepay, false, null);
 
             // verify transactions
             verifyTransactions(loanId, //
@@ -754,4 +753,109 @@ public class LoanPointInTimeTest extends 
BaseLoanIntegrationTest {
             );
         });
     }
+
+    @Test
+    public void 
test_LoanPointInTimeDataWorks_ForArrearsDataCalculation_ForFutureDate_WithInterest()
 {
+        AtomicReference<Long> aLoanId = new AtomicReference<>();
+
+        runAt("01 January 2023", () -> {
+            // Create Client
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 3;
+            int repaymentEvery = 1;
+
+            // Create charges
+            double charge1Amount = 1.0;
+            double charge2Amount = 1.5;
+            Long charge1Id = createDisbursementPercentageCharge(charge1Amount);
+            Long charge2Id = createDisbursementPercentageCharge(charge2Amount);
+
+            // Create Loan Product
+            double interestRatePerPeriod = 10.0;
+            PostLoanProductsRequest product = 
createOnePeriod30DaysPeriodicAccrualProduct(interestRatePerPeriod) //
+                    .numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .installmentAmountInMultiplesOf(null) //
+                    
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+                    
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
+                    .isInterestRecalculationEnabled(true)//
+                    .recalculationRestFrequencyInterval(1)//
+                    
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
+                    
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
+                    .allowPartialPeriodInterestCalcualtion(false)//
+                    .disallowExpectedDisbursements(false)//
+                    .allowApprovedDisbursedAmountsOverApplied(false)//
+                    .overAppliedNumber(null)//
+                    .overAppliedCalculationType(null)//
+                    .multiDisburseLoan(null)//
+                    .charges(List.of(new 
LoanProductChargeData().id(charge1Id), new 
LoanProductChargeData().id(charge2Id)));//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            double amount = 5000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "01 January 2023", amount, numberOfRepayments)//
+                    .repaymentEvery(repaymentEvery)//
+                    
.interestRatePerPeriod(BigDecimal.valueOf(interestRatePerPeriod)).loanTermFrequency(numberOfRepayments)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .interestType(InterestType.DECLINING_BALANCE)//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+                    .charges(List.of(//
+                            new 
PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)),
 //
+                            new 
PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))//
+            ));//
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            aLoanId.getAndSet(approvedLoanResult.getLoanId());
+            Long loanId = aLoanId.get();
+
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 
2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023"), //
+                    transaction(125.0, "Repayment (at time of disbursement)", 
"01 January 2023") //
+            );
+        });
+
+        runAt("05 March 2023", () -> {
+            Long loanId = aLoanId.get();
+
+            // repay
+            addRepaymentForLoan(loanId, 5897.89, "05 March 2023");
+
+            // verify transactions
+            verifyTransactions(loanId, //
+                    transaction(5000.0, "Disbursement", "01 January 2023"), //
+                    transaction(125.0, "Repayment (at time of disbursement)", 
"01 January 2023"), //
+                    transaction(5897.89, "Repayment", "05 March 2023"), //
+                    transaction(897.89, "Accrual", "05 March 2023") //
+            );
+        });
+
+        runAt("05 June 2023", () -> {
+            Long loanId = aLoanId.get();
+
+            LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, 
"05 June 2023");
+
+            verifyOutstanding(pointInTimeData, outstanding(0.0, 0.0, 0.0, 0.0, 
0.0));
+            verifyArrears(pointInTimeData, false, null);
+            
assertThat(pointInTimeData.getArrears().getPrincipalOverdue()).isZero();
+            assertThat(pointInTimeData.getArrears().getFeeOverdue()).isZero();
+            
assertThat(pointInTimeData.getArrears().getInterestOverdue()).isZero();
+            
assertThat(pointInTimeData.getArrears().getPenaltyOverdue()).isZero();
+            
assertThat(pointInTimeData.getArrears().getTotalOverdue()).isZero();
+        });
+    }
 }

Reply via email to