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

commit 6058837af2a42848d9f85b648a7bd39d5785ede5
Author: Attila Budai <[email protected]>
AuthorDate: Tue Feb 24 22:04:21 2026 +0100

    FINERACT-2421: fix reage accumulating periods
---
 .../resources/features/LoanReAgingPreview.feature  |  56 +++++
 .../loanproduct/calc/ProgressiveEMICalculator.java |  29 +++
 .../data/ProgressiveLoanInterestScheduleModel.java |  14 +-
 .../loan/reaging/LoanReAgingIntegrationTest.java   | 247 +++++++++++++++++++++
 4 files changed, 343 insertions(+), 3 deletions(-)

diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAgingPreview.feature
 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAgingPreview.feature
index 0cf1deb9b0..07e8801c9d 100644
--- 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanReAgingPreview.feature
+++ 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanReAgingPreview.feature
@@ -2870,4 +2870,60 @@ Feature: LoanReAgingPreview
     When Loan Pay-off is made on "24 February 2026"
     Then Loan is closed with zero outstanding balance and it's all 
installments have obligations met
 
+  @TestRailId:C6000 @AdvancedPaymentAllocation
+  Scenario: Re-aging preview should not accumulate 1-day periods after 
repeated re-aging across month-end (PS-3004)
+    When Admin sets the business date to "28 January 2026"
+    When Admin creates a client with random data
+    When Admin creates a fully customized loan with the following data:
+      | LoanProduct                                                            
  | submitted on date | with Principal | ANNUAL interest rate % | interest type 
    | interest calculation period | amortization type  | loanTermFrequency | 
loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | 
numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | 
interest free period | Payment strategy            |
+      | 
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION | 28 
January 2026   | 100            | 7                       | DECLINING_BALANCE | 
DAILY                       | EQUAL_INSTALLMENTS | 6                 | MONTHS   
             | 1              | MONTHS                 | 6                  | 0 
                      | 0                      | 0                    | 
ADVANCED_PAYMENT_ALLOCATION |
+    And Admin successfully approves the loan on "28 January 2026" with "100" 
amount and expected disbursement date on "28 January 2026"
+    When Admin successfully disburse the loan on "28 January 2026" with "100" 
EUR transaction amount
+#   --- Re-age 4 times across late-January dates ---
+    When Admin creates a Loan re-aging transaction with the following data:
+      | frequencyNumber | frequencyType | startDate        | 
numberOfInstallments |
+      | 1               | MONTHS        | 28 February 2026 | 6                 
   |
+    When Admin sets the business date to "29 January 2026"
+    And Admin creates a Loan re-aging transaction with the following data:
+      | frequencyNumber | frequencyType | startDate        | 
numberOfInstallments |
+      | 1               | MONTHS        | 28 February 2026 | 6                 
   |
+    When Admin sets the business date to "30 January 2026"
+    And Admin creates a Loan re-aging transaction with the following data:
+      | frequencyNumber | frequencyType | startDate        | 
numberOfInstallments |
+      | 1               | MONTHS        | 28 February 2026 | 6                 
   |
+    When Admin sets the business date to "31 January 2026"
+    And Admin creates a Loan re-aging transaction with the following data:
+      | frequencyNumber | frequencyType | startDate        | 
numberOfInstallments |
+      | 1               | MONTHS        | 28 February 2026 | 6                 
   |
+#   --- Verify actual schedule: should be 8 periods (1 disbursement + 1 stub + 
6 re-aged), NOT 12+ ---
+    Then Loan Repayment schedule has 7 periods, with the following data for 
periods:
+      | Nr | Days | Date              | Paid date        | Balance of loan | 
Principal due | Interest | Fees | Penalties | Due   | Paid | In advance | Late 
| Outstanding |
+      |    |      | 28 January 2026   |                  | 100.0           |   
            |          | 0.0  |           | 0.0   | 0.0  |            |      |  
           |
+      | 1  | 3    | 31 January 2026   | 28 January 2026  | 100.0           | 
0.0           | 0.0      | 0.0  | 0.0       | 0.0   | 0.0  | 0.0        | 0.0  
| 0.0         |
+      | 2  | 28   | 28 February 2026  |                  | 83.62           | 
16.38         | 0.64     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 3  | 28   | 28 March 2026     |                  | 67.09           | 
16.53         | 0.49     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 4  | 31   | 28 April 2026     |                  | 50.46           | 
16.63         | 0.39     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 5  | 30   | 28 May 2026       |                  | 33.73           | 
16.73         | 0.29     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 6  | 31   | 28 June 2026      |                  | 16.91           | 
16.82         | 0.2      | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 7  | 30   | 28 July 2026      |                  | 0.0             | 
16.91         | 0.1      | 0.0  | 0.0       | 17.01 | 0.0  | 0.0        | 0.0  
| 17.01       |
+#   --- Now move to February and call re-aging preview ---
+    When Admin sets the business date to "01 February 2026"
+    And Admin creates a Loan re-aging preview by Loan external ID with the 
following data:
+      | frequencyNumber | frequencyType | startDate        | 
numberOfInstallments |
+      | 1               | MONTHS        | 28 February 2026 | 6                 
   |
+#   --- Preview should also show correct number of periods, NOT duplicated ---
+    Then Loan Re-Aged Repayment schedule preview has 7 periods, with the 
following data for periods:
+      | Nr | Days | Date              | Paid date       | Balance of loan | 
Principal due | Interest | Fees | Penalties | Due   | Paid | In advance | Late 
| Outstanding |
+      |    |      | 28 January 2026   |                 | 100.0           | 
0.0           | 0.0      | 0.0  | 0.0       | 0.0   | 0.0  |            |      
|             |
+      | 1  | 4    | 01 February 2026  | 28 January 2026 | 100.0           | 
0.0           | 0.0      | 0.0  | 0.0       | 0.0   | 0.0  | 0.0        | 0.0  
| 0.0         |
+      | 2  | 27   | 28 February 2026  |                 | 83.62           | 
16.38         | 0.64     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 3  | 28   | 28 March 2026     |                 | 67.09           | 
16.53         | 0.49     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 4  | 31   | 28 April 2026     |                 | 50.46           | 
16.63         | 0.39     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 5  | 30   | 28 May 2026       |                 | 33.73           | 
16.73         | 0.29     | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 6  | 31   | 28 June 2026      |                 | 16.91           | 
16.82         | 0.2      | 0.0  | 0.0       | 17.02 | 0.0  | 0.0        | 0.0  
| 17.02       |
+      | 7  | 30   | 28 July 2026      |                 | 0.0             | 
16.91         | 0.1      | 0.0  | 0.0       | 17.01 | 0.0  | 0.0        | 0.0  
| 17.01       |
+
+    When Loan Pay-off is made on "01 February 2026"
+    Then Loan is closed with zero outstanding balance and it's all 
installments have obligations met
+
 
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
index b5de8612bb..77c9a48a5d 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -718,6 +718,8 @@ public final class ProgressiveEMICalculator implements 
EMICalculator {
 
         
moveOutstandingAmountsFromPeriodsBeforeTransactionDate(scheduleModel.repaymentPeriods(),
 targetDate);
 
+        collapseIntermediateStubPeriods(scheduleModel);
+
         final ProgressiveLoanInterestScheduleModel 
temporaryReAgedScheduleModel = 
generateTemporaryScheduleModel(loanApplicationTerms, mc,
                 reAgePeriodStartDate, reAgePeriodStartDate);
 
@@ -1082,6 +1084,31 @@ public final class ProgressiveEMICalculator implements 
EMICalculator {
         });
     }
 
+    private void collapseIntermediateStubPeriods(final 
ProgressiveLoanInterestScheduleModel scheduleModel) {
+        final List<RepaymentPeriod> periods = scheduleModel.repaymentPeriods();
+        if (periods.size() <= 1) {
+            return;
+        }
+        // Only collapse if ALL periods are zero-EMI stubs (no principal due, 
no interest due, no paid amounts).
+        // This handles the repeated re-aging case where each re-age leaves 
behind a 1-day stub period,
+        // without affecting legitimate paid installments in 
multi-disbursement scenarios.
+        final boolean allPeriodsAreStubs = periods.stream()
+                .allMatch(rp -> rp.getEmi().isZero() && 
rp.getDuePrincipal().isZero() && rp.getDueInterest().isZero());
+        if (!allPeriodsAreStubs) {
+            return;
+        }
+        final RepaymentPeriod firstPeriod = periods.getFirst();
+        final RepaymentPeriod lastPeriod = periods.getLast();
+        final LocalDate lastDueDate = lastPeriod.getDueDate();
+
+        firstPeriod.setDueDate(lastDueDate);
+        firstPeriod.getInterestPeriods().getLast().setDueDate(lastDueDate);
+
+        periods.subList(1, periods.size()).clear();
+
+        calculateRateFactorForRepaymentPeriod(firstPeriod, scheduleModel);
+    }
+
     private void 
calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel 
scheduleModel, LocalDate tillDate) {
 
         Money totalDuePaidDiff = 
scheduleModel.getTotalDuePrincipal().minus(scheduleModel.getTotalPaidPrincipal());
@@ -2012,6 +2039,8 @@ public final class ProgressiveEMICalculator implements 
EMICalculator {
             rp.setInterestMovedDownward(true);
         });
 
+        collapseIntermediateStubPeriods(interestSchedule);
+
         if (!originalMaturityDate.isBefore(transactionDate)) {
             
createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule,
                     paidBalancesFromTransactionDate.getOutstandingPrincipal(), 
paidBalancesFromTransactionDate.getOutstandingInterest(),
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
index c02a23e2b0..1dabb865da 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
@@ -167,10 +167,18 @@ public class ProgressiveLoanInterestScheduleModel {
         if (repaymentPeriodDueDate == null) {
             return Optional.empty();
         }
-        return repaymentPeriods.stream()//
-                .filter(repaymentPeriodItem -> 
DateUtils.isEqual(repaymentPeriodItem.getFromDate(), repaymentPeriodFromDate)
-                        && DateUtils.isEqual(repaymentPeriodItem.getDueDate(), 
repaymentPeriodDueDate))//
+        // Exact match first
+        Optional<RepaymentPeriod> result = repaymentPeriods.stream()
+                .filter(rp -> DateUtils.isEqual(rp.getFromDate(), 
repaymentPeriodFromDate)
+                        && DateUtils.isEqual(rp.getDueDate(), 
repaymentPeriodDueDate))
                 .findFirst();
+        if (result.isEmpty()) {
+            // Fallback: find a period that encompasses the requested date 
range
+            // This handles collapsed stub periods where multiple periods were 
merged into one
+            result = repaymentPeriods.stream().filter(rp -> 
!DateUtils.isAfter(rp.getFromDate(), repaymentPeriodFromDate)
+                    && !DateUtils.isBefore(rp.getDueDate(), 
repaymentPeriodDueDate)).findFirst();
+        }
+        return result;
     }
 
     public List<RepaymentPeriod> getRelatedRepaymentPeriods(final LocalDate 
calculateFromRepaymentPeriodDueDate) {
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
index 2b2c728aff..a258b0c0fd 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
@@ -24,12 +24,15 @@ import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import io.restassured.path.json.JsonPath;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.HashMap;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicLong;
+import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import org.apache.fineract.client.models.PostChargesResponse;
 import org.apache.fineract.client.models.PostLoanProductsRequest;
 import org.apache.fineract.client.models.PostLoanProductsResponse;
@@ -1024,6 +1027,250 @@ public class LoanReAgingIntegrationTest extends 
BaseLoanIntegrationTest {
         });
     }
 
+    @Test
+    public void test_LoanReAge_RepeatedReAgeDoesNotCreateDuplicatePeriods() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("28 January 2026", () -> {
+            // Create Client
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 6;
+            int repaymentEvery = 1;
+
+            // Create interest-bearing progressive loan product
+            PostLoanProductsRequest product = create4IProgressive() //
+                    .numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L); 
//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "28 January 2026", amount, numberOfRepayments)//
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .interestRatePerPeriod(BigDecimal.valueOf(10.0))//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "28 January 2026"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+
+            // Disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(amount), "28 January 
2026");
+
+            createdLoanId.set(loanId);
+
+            // First re-age
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        // Second re-age on next day
+        runAt("29 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        // Third re-age on next day
+        runAt("30 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        // Fourth re-age on next day
+        runAt("31 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+
+            // Verify: should have 8 periods total (1 disbursement + 1 stub + 
6 re-aged installments)
+            // NOT 12+ periods with spurious stubs from each intermediate reAge
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            List<GetLoansLoanIdRepaymentPeriod> periods = 
loanDetails.getRepaymentSchedule().getPeriods();
+
+            assertEquals(8, periods.size(), "Expected 8 periods (1 
disbursement + 1 stub + 6 re-aged) but got " + periods.size());
+
+            // Verify due dates are correct
+            assertEquals(LocalDate.of(2026, 1, 28), 
periods.get(0).getDueDate()); // disbursement
+            assertEquals(LocalDate.of(2026, 1, 31), 
periods.get(1).getDueDate()); // stub
+            assertEquals(LocalDate.of(2026, 2, 28), 
periods.get(2).getDueDate()); // 1st re-aged
+            assertEquals(LocalDate.of(2026, 3, 28), 
periods.get(3).getDueDate()); // 2nd re-aged
+            assertEquals(LocalDate.of(2026, 4, 28), 
periods.get(4).getDueDate()); // 3rd re-aged
+            assertEquals(LocalDate.of(2026, 5, 28), 
periods.get(5).getDueDate()); // 4th re-aged
+            assertEquals(LocalDate.of(2026, 6, 28), 
periods.get(6).getDueDate()); // 5th re-aged
+            assertEquals(LocalDate.of(2026, 7, 28), 
periods.get(7).getDueDate()); // 6th re-aged
+
+            checkMaturityDates(loanId, LocalDate.of(2026, 7, 28), 
LocalDate.of(2026, 7, 28));
+        });
+    }
+
+    @Test
+    public void test_LoanReAge_RepeatedReAge_COBAccrualDoesNotFail() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("28 January 2026", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 6;
+            int repaymentEvery = 1;
+
+            PostLoanProductsRequest product = create4IProgressive() //
+                    .numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L); 
//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "28 January 2026", amount, numberOfRepayments)//
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .interestRatePerPeriod(BigDecimal.valueOf(10.0))//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "28 January 2026"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+
+            disburseLoan(loanId, BigDecimal.valueOf(amount), "28 January 
2026");
+
+            createdLoanId.set(loanId);
+
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("29 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("30 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("31 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("01 February 2026", () -> {
+            long loanId = createdLoanId.get();
+
+            // Execute inline COB - this should not fail with 
NoSuchElementException
+            executeInlineCOB(loanId);
+
+            // Verify loan schedule still has 8 periods (1 disbursement + 1 
stub + 6 re-aged)
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            List<GetLoansLoanIdRepaymentPeriod> periods = 
loanDetails.getRepaymentSchedule().getPeriods();
+
+            assertEquals(8, periods.size(), "Expected 8 periods (1 
disbursement + 1 stub + 6 re-aged) but got " + periods.size());
+
+            // Verify loan is still active (COB did not crash)
+            assertEquals(LoanStatus.ACTIVE.getValue(), 
loanDetails.getStatus().getId().intValue());
+        });
+    }
+
+    @Test
+    public void test_LoanReAge_RepeatedReAge_PreviewShowsCorrectPeriods() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("28 January 2026", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 6;
+            int repaymentEvery = 1;
+
+            PostLoanProductsRequest product = create4IProgressive() //
+                    .numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L); 
//
+
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            double amount = 1000.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, 
loanProductId, "28 January 2026", amount, numberOfRepayments)//
+                    
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .interestRatePerPeriod(BigDecimal.valueOf(10.0))//
+                    
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY);
+
+            PostLoansResponse postLoansResponse = 
loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = 
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "28 January 2026"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+
+            disburseLoan(loanId, BigDecimal.valueOf(amount), "28 January 
2026");
+
+            createdLoanId.set(loanId);
+
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("29 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("30 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+        });
+
+        runAt("31 January 2026", () -> {
+            long loanId = createdLoanId.get();
+            reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "28 
February 2026", 6, null);
+
+            // Verify actual schedule has 8 periods
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            List<GetLoansLoanIdRepaymentPeriod> periods = 
loanDetails.getRepaymentSchedule().getPeriods();
+
+            assertEquals(8, periods.size(), "Expected 8 periods (1 
disbursement + 1 stub + 6 re-aged) but got " + periods.size());
+        });
+
+        runAt("01 February 2026", () -> {
+            long loanId = createdLoanId.get();
+
+            // Call preview API via REST
+            String previewUrl = "/fineract-provider/api/v1/loans/" + loanId + 
"/transactions/reage-preview" //
+                    + 
"?frequencyType=MONTHS&frequencyNumber=1&startDate=28+February+2026&numberOfInstallments=6"
 //
+                    + "&dateFormat=dd+MMMM+yyyy&locale=en&" + 
Utils.TENANT_IDENTIFIER;
+
+            String jsonResponse = Utils.performServerGet(requestSpec, 
responseSpec, previewUrl);
+
+            // Parse the periods array from the JSON response
+            List<HashMap<String, Object>> previewPeriods = 
JsonPath.from(jsonResponse).getList("periods");
+
+            assertNotNull(previewPeriods, "Preview response should contain 
periods");
+            assertEquals(8, previewPeriods.size(),
+                    "Preview should have 8 periods (1 disbursement + 1 stub + 
6 re-aged) but got " + previewPeriods.size());
+        });
+    }
+
     private HashMap<String, Object> getReAgeTemplate(Long loanId) {
         final String GET_REAGE_TEMPLATE_URL = 
"/fineract-provider/api/v1/loans/" + loanId + 
"/transactions/template?command=reAge&"
                 + Utils.TENANT_IDENTIFIER;

Reply via email to