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

adamsaghy pushed a commit to branch release/1.13.1
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit 40a044ecec413e8bd7a439048a08f856c8334e81
Author: Attila Budai <[email protected]>
AuthorDate: Mon Oct 13 23:02:30 2025 +0200

    FINERACT-2326: fix delinquent days & delinquency date after delinquency 
pause calculations
---
 .../resources/features/LoanDelinquency.feature     |  15 +-
 .../helper/DelinquencyEffectivePauseHelper.java    |   3 +
 .../DelinquencyEffectivePauseHelperImpl.java       |  24 +
 .../helper/InstallmentDelinquencyAggregator.java   |  87 ++++
 .../DelinquencyReadPlatformServiceImpl.java        |  33 +-
 .../service/LoanDelinquencyDomainServiceImpl.java  |  53 +--
 .../InstallmentDelinquencyAggregatorTest.java      | 278 ++++++++++++
 .../LoanDelinquencyDomainServiceTest.java          | 116 ++++-
 .../DelinquencyActionIntegrationTests.java         | 496 +++++++++++++++++++++
 .../DelinquencyBucketsIntegrationTest.java         |  30 +-
 10 files changed, 1061 insertions(+), 74 deletions(-)

diff --git 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature
index 5967ea805c..d12375f371 100644
--- 
a/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature
+++ 
b/fineract-e2e-tests-runner/src/test/resources/features/LoanDelinquency.feature
@@ -610,6 +610,7 @@ Feature: LoanDelinquency
 #    --- Grace period applied only on Loan level, not on installment level ---
     Then Loan has the following INSTALLMENT level delinquency data:
       | rangeId | Range   | Amount |
+      | 1       | RANGE_1 | 250.00 |
       | 2       | RANGE_3 | 250.00 |
 
   @TestRailId:C3000
@@ -728,8 +729,7 @@ Feature: LoanDelinquency
       | RANGE_3        | 750.0            | 04 October 2023 | 30             | 
43          |
     Then Loan has the following INSTALLMENT level delinquency data:
       | rangeId | Range    | Amount |
-      | 1       | RANGE_1  | 250.00 |
-      | 2       | RANGE_3  | 250.00 |
+      | 2       | RANGE_3  | 500.00 |
       | 3       | RANGE_30 | 250.00 |
 #    --- Second delinquency pause ---
     When Admin sets the business date to "14 November 2023"
@@ -749,8 +749,7 @@ Feature: LoanDelinquency
       | RANGE_3        | 750.0            | 04 October 2023 | 31             | 
44          |
     Then Loan has the following INSTALLMENT level delinquency data:
       | rangeId | Range    | Amount |
-      | 1       | RANGE_1  | 250.00 |
-      | 2       | RANGE_3  | 250.00 |
+      | 2       | RANGE_3  | 500.00 |
       | 3       | RANGE_30 | 250.00 |
     Then Installment level delinquency event has correct data
 #    --- Second delinquency ends ---
@@ -770,8 +769,7 @@ Feature: LoanDelinquency
       | RANGE_3       | 1000.0           | 04 October 2023 | 31             | 
60          |
     Then Loan has the following INSTALLMENT level delinquency data:
       | rangeId | Range    | Amount |
-      | 1       | RANGE_1  | 250.00 |
-      | 2       | RANGE_3  | 250.00 |
+      | 2       | RANGE_3  | 500.00 |
       | 3       | RANGE_30 | 250.00 |
 #    --- Delinquency runs again ---
     When Admin sets the business date to "01 December 2023"
@@ -790,6 +788,7 @@ Feature: LoanDelinquency
       | RANGE_30       | 1000.0           | 04 October 2023 | 32             | 
61          |
     Then Loan has the following INSTALLMENT level delinquency data:
       | rangeId | Range    | Amount |
+      | 1       | RANGE_1  | 250.00 |
       | 2       | RANGE_3  | 500.00 |
       | 3       | RANGE_30 | 250.00 |
     Then Installment level delinquency event has correct data
@@ -995,11 +994,11 @@ Feature: LoanDelinquency
       | RESUME | 25 October 2023 |                 |
     Then Loan has the following LOAN level delinquency data:
       | classification | delinquentAmount | delinquentDate  | delinquentDays | 
pastDueDays |
-      | RANGE_3        | 500.0            | 19 October 2023 | 8              | 
30          |
+      | RANGE_3        | 500.0            | 19 October 2023 | 18             | 
30          |
 #    --- Grace period applied only on Loan level, not on installment level ---
     Then Loan has the following INSTALLMENT level delinquency data:
       | rangeId | Range   | Amount |
-      | 2       | RANGE_3 | 250.00 |
+      | 2       | RANGE_3 | 500.00 |
     Then Installment level delinquency event has correct data
 
   @TestRailId:C3013
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelper.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelper.java
index e236d84222..a5722ff75c 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelper.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelper.java
@@ -28,4 +28,7 @@ public interface DelinquencyEffectivePauseHelper {
     List<LoanDelinquencyActionData> 
calculateEffectiveDelinquencyList(List<LoanDelinquencyAction> 
savedDelinquencyActions);
 
     Long getPausedDaysBeforeDate(List<LoanDelinquencyActionData> 
effectiveDelinquencyList, LocalDate date);
+
+    Long getPausedDaysWithinRange(List<LoanDelinquencyActionData> 
effectiveDelinquencyList, LocalDate startInclusive,
+            LocalDate endExclusive);
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelperImpl.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelperImpl.java
index eeb64bff5d..f400a836fd 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelperImpl.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/DelinquencyEffectivePauseHelperImpl.java
@@ -66,6 +66,30 @@ public class DelinquencyEffectivePauseHelperImpl implements 
DelinquencyEffective
         return Long.sum(pausedDaysClosedPausePeriods, 
pausedDaysRunningPausePeriods);
     }
 
+    @Override
+    public Long getPausedDaysWithinRange(List<LoanDelinquencyActionData> 
effectiveDelinquencyList, LocalDate startInclusive,
+            LocalDate endExclusive) {
+        if (startInclusive == null || endExclusive == null || 
!startInclusive.isBefore(endExclusive)) {
+            return 0L;
+        }
+        return effectiveDelinquencyList.stream().map(pausePeriod -> {
+            LocalDate pauseStart = pausePeriod.getStartDate();
+            LocalDate pauseEnd = 
Optional.ofNullable(pausePeriod.getEndDate()).orElse(endExclusive);
+            if (pauseStart == null || !pauseStart.isBefore(endExclusive)) {
+                return 0L;
+            }
+            if (!pauseEnd.isAfter(startInclusive)) {
+                return 0L;
+            }
+            LocalDate overlapStart = pauseStart.isAfter(startInclusive) ? 
pauseStart : startInclusive;
+            LocalDate overlapEnd = pauseEnd.isBefore(endExclusive) ? pauseEnd 
: endExclusive;
+            if (!overlapStart.isBefore(overlapEnd)) {
+                return 0L;
+            }
+            return DateUtils.getDifferenceInDays(overlapStart, overlapEnd);
+        }).reduce(0L, Long::sum);
+    }
+
     private Optional<LoanDelinquencyAction> 
findMatchingResume(LoanDelinquencyAction pause, List<LoanDelinquencyAction> 
resumes) {
         if (resumes != null && resumes.size() > 0) {
             for (LoanDelinquencyAction resume : resumes) {
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/InstallmentDelinquencyAggregator.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/InstallmentDelinquencyAggregator.java
new file mode 100644
index 0000000000..3ddcb36348
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/helper/InstallmentDelinquencyAggregator.java
@@ -0,0 +1,87 @@
+/**
+ * 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.delinquency.helper;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import 
org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
+import 
org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency;
+
+/**
+ * Static utility class for aggregating installment-level delinquency data.
+ *
+ * @see InstallmentLevelDelinquency
+ * @see LoanInstallmentDelinquencyTagData
+ */
+public final class InstallmentDelinquencyAggregator {
+
+    private InstallmentDelinquencyAggregator() {}
+
+    /**
+     * Aggregates installment-level delinquency data by rangeId and sorts by 
minimumAgeDays.
+     *
+     * This method performs two key operations: 1. Groups installments by 
delinquency rangeId and sums delinquentAmount
+     * for installments with the same rangeId 2. Sorts the aggregated results 
by minimumAgeDays in ascending order
+     *
+     * @param installmentData
+     *            Collection of installment delinquency data to aggregate
+     * @return Sorted list of aggregated delinquency data, empty list if input 
is null or empty
+     */
+    public static List<InstallmentLevelDelinquency> 
aggregateAndSort(Collection<LoanInstallmentDelinquencyTagData> installmentData) 
{
+
+        if (installmentData == null || installmentData.isEmpty()) {
+            return List.of();
+        }
+
+        Collection<InstallmentLevelDelinquency> aggregated = 
installmentData.stream().map(InstallmentLevelDelinquency::from)
+                
.collect(Collectors.groupingBy(InstallmentLevelDelinquency::getRangeId, 
delinquentAmountSummingCollector())).values()
+                .stream().map(opt -> opt.orElseThrow(() -> new 
IllegalStateException("Unexpected empty Optional in aggregation"))).toList();
+
+        return aggregated.stream().sorted(Comparator.comparing(ild -> 
Optional.ofNullable(ild.getMinimumAgeDays()).orElse(0))).toList();
+    }
+
+    /**
+     * Creates a custom collector that sums delinquent amounts while 
preserving range metadata.
+     *
+     * This collector uses the reducing operation to combine multiple 
InstallmentLevelDelinquency objects with the same
+     * rangeId. It preserves the range classification (rangeId, 
classification, minimumAgeDays, maximumAgeDays) while
+     * summing the delinquentAmount fields.
+     *
+     * Note: This uses the 1-argument reducing() variant which returns 
Optional<T> to avoid the identity value bug that
+     * would cause amounts to be incorrectly doubled when aggregating single 
installments.
+     *
+     * @return Collector that combines InstallmentLevelDelinquency objects by 
summing amounts
+     */
+    private static Collector<InstallmentLevelDelinquency, ?, 
Optional<InstallmentLevelDelinquency>> delinquentAmountSummingCollector() {
+        return Collectors.reducing((item1, item2) -> {
+            final InstallmentLevelDelinquency result = new 
InstallmentLevelDelinquency();
+            result.setRangeId(item1.getRangeId());
+            result.setClassification(item1.getClassification());
+            result.setMaximumAgeDays(item1.getMaximumAgeDays());
+            result.setMinimumAgeDays(item1.getMinimumAgeDays());
+            
result.setDelinquentAmount(MathUtil.add(item1.getDelinquentAmount(), 
item2.getDelinquentAmount()));
+            return result;
+        });
+    }
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
index 4ad69707b3..6b9f27b194 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
@@ -27,8 +27,6 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Collector;
-import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import 
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -49,6 +47,7 @@ import 
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistor
 import 
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
 import 
org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
 import 
org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
+import 
org.apache.fineract.portfolio.delinquency.helper.InstallmentDelinquencyAggregator;
 import 
org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
 import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
 import 
org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
@@ -260,36 +259,12 @@ public class DelinquencyReadPlatformServiceImpl 
implements DelinquencyReadPlatfo
         Collection<LoanInstallmentDelinquencyTagData> 
loanInstallmentDelinquencyTagData = 
retrieveLoanInstallmentsCurrentDelinquencyTag(
                 loanId);
         if (loanInstallmentDelinquencyTagData != null && 
!loanInstallmentDelinquencyTagData.isEmpty()) {
-
-            // installment level delinquency grouped by rangeId, and summed up 
the delinquent amount
-            Collection<InstallmentLevelDelinquency> 
installmentLevelDelinquencies = loanInstallmentDelinquencyTagData.stream()
-                    .map(InstallmentLevelDelinquency::from)
-                    
.collect(Collectors.groupingBy(InstallmentLevelDelinquency::getRangeId, 
delinquentAmountSummingCollector())).values();
-
-            // sort this based on minimum days, so ranges will be delivered in 
ascending order
-            List<InstallmentLevelDelinquency> sorted = 
installmentLevelDelinquencies.stream().sorted((o1, o2) -> {
-                Integer first = 
Optional.ofNullable(o1.getMinimumAgeDays()).orElse(0);
-                Integer second = 
Optional.ofNullable(o2.getMinimumAgeDays()).orElse(0);
-                return first.compareTo(second);
-            }).toList();
-
-            collectionData.setInstallmentLevelDelinquency(sorted);
+            List<InstallmentLevelDelinquency> aggregated = 
InstallmentDelinquencyAggregator
+                    .aggregateAndSort(loanInstallmentDelinquencyTagData);
+            collectionData.setInstallmentLevelDelinquency(aggregated);
         }
     }
 
-    @NonNull
-    private static Collector<InstallmentLevelDelinquency, ?, 
InstallmentLevelDelinquency> delinquentAmountSummingCollector() {
-        return Collectors.reducing(new InstallmentLevelDelinquency(), (item1, 
item2) -> {
-            final InstallmentLevelDelinquency result = new 
InstallmentLevelDelinquency();
-            
result.setRangeId(Optional.ofNullable(item1.getRangeId()).orElse(item2.getRangeId()));
-            
result.setClassification(Optional.ofNullable(item1.getClassification()).orElse(item2.getClassification()));
-            
result.setMaximumAgeDays(Optional.ofNullable(item1.getMaximumAgeDays()).orElse(item2.getMaximumAgeDays()));
-            
result.setMinimumAgeDays(Optional.ofNullable(item1.getMinimumAgeDays()).orElse(item2.getMinimumAgeDays()));
-            
result.setDelinquentAmount(MathUtil.add(item1.getDelinquentAmount(), 
item2.getDelinquentAmount()));
-            return result;
-        });
-    }
-
     void enrichWithDelinquencyPausePeriodInfo(CollectionData collectionData, 
Collection<LoanDelinquencyActionData> effectiveDelinquencyList,
             LocalDate businessDate) {
         List<DelinquencyPausePeriod> result = 
effectiveDelinquencyList.stream() //
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
index dd8a2218e6..e4fc1eb187 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
@@ -119,14 +119,15 @@ public class LoanDelinquencyDomainServiceImpl implements 
LoanDelinquencyDomainSe
         log.debug("Loan id {} with overdue since date {} and outstanding 
amount {}", loan.getId(), overdueSinceDate, outstandingAmount);
 
         long overdueDays = 0L;
+        LocalDate overdueSinceDateForCalculation = overdueSinceDate;
         if (overdueSinceDate != null) {
             overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, 
businessDate);
             if (overdueDays < 0) {
                 overdueDays = 0L;
             }
             collectionData.setPastDueDays(overdueDays);
-            overdueSinceDate = 
overdueSinceDate.plusDays(graceDays.longValue());
-            collectionData.setDelinquentDate(overdueSinceDate);
+            LocalDate delinquentStartDate = 
overdueSinceDate.plusDays(graceDays.longValue());
+            collectionData.setDelinquentDate(delinquentStartDate);
         }
         collectionData.setDelinquentAmount(outstandingAmount);
         collectionData.setDelinquentPrincipal(delinquentPrincipal);
@@ -134,11 +135,8 @@ public class LoanDelinquencyDomainServiceImpl implements 
LoanDelinquencyDomainSe
         collectionData.setDelinquentFee(delinquentFee);
         collectionData.setDelinquentPenalty(delinquentPenalty);
 
-        collectionData.setDelinquentDays(0L);
-        final long delinquentDays = overdueDays - graceDays;
-        if (delinquentDays > 0) {
-            calculateDelinquentDays(effectiveDelinquencyList, businessDate, 
collectionData, delinquentDays);
-        }
+        calculateAndSetDelinquentDays(collectionData, overdueDays, graceDays, 
effectiveDelinquencyList, businessDate,
+                overdueSinceDateForCalculation);
 
         log.debug("Result: {}", collectionData);
         return collectionData;
@@ -200,31 +198,22 @@ public class LoanDelinquencyDomainServiceImpl implements 
LoanDelinquencyDomainSe
         log.debug("Loan id {} with overdue since date {} and outstanding 
amount {}", loan.getId(), overdueSinceDate, outstandingAmount);
 
         long overdueDays = 0L;
+        LocalDate overdueSinceDateForCalculation = overdueSinceDate;
         if (overdueSinceDate != null) {
             overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, 
businessDate);
             if (overdueDays < 0) {
                 overdueDays = 0L;
             }
             collectionData.setPastDueDays(overdueDays);
-            overdueSinceDate = 
overdueSinceDate.plusDays(graceDays.longValue());
-            collectionData.setDelinquentDate(overdueSinceDate);
+            LocalDate delinquentStartDate = 
overdueSinceDate.plusDays(graceDays.longValue());
+            collectionData.setDelinquentDate(delinquentStartDate);
         }
         collectionData.setDelinquentAmount(outstandingAmount);
-        collectionData.setDelinquentDays(0L);
-        final long delinquentDays = overdueDays - graceDays;
-        if (delinquentDays > 0) {
-            calculateDelinquentDays(effectiveDelinquencyList, businessDate, 
collectionData, delinquentDays);
-        }
+        calculateAndSetDelinquentDays(collectionData, overdueDays, graceDays, 
effectiveDelinquencyList, businessDate,
+                overdueSinceDateForCalculation);
         return new LoanDelinquencyData(collectionData, 
loanInstallmentsCollectionData);
     }
 
-    private void calculateDelinquentDays(List<LoanDelinquencyActionData> 
effectiveDelinquencyList, LocalDate businessDate,
-            CollectionData collectionData, Long delinquentDays) {
-        Long pausedDays = 
delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList,
 businessDate);
-        Long calculatedDelinquentDays = delinquentDays - pausedDays;
-        collectionData.setDelinquentDays(calculatedDelinquentDays > 0 ? 
calculatedDelinquentDays : 0L);
-    }
-
     private CollectionData getInstallmentOverdueCollectionData(final Loan 
loan, final LoanRepaymentScheduleInstallment installment,
             final List<LoanDelinquencyActionData> effectiveDelinquencyList, 
final List<LoanTransaction> chargebackTransactions) {
         final LocalDate businessDate = DateUtils.getBusinessLocalDate();
@@ -248,6 +237,7 @@ public class LoanDelinquencyDomainServiceImpl implements 
LoanDelinquencyDomainSe
 
         // Grace days are not considered for installment level delinquency 
calculation currently.
         long overdueDays = 0L;
+        LocalDate overdueSinceDateForCalculation = overdueSinceDate;
         if (overdueSinceDate != null) {
             overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, 
businessDate);
             if (overdueDays < 0) {
@@ -257,11 +247,8 @@ public class LoanDelinquencyDomainServiceImpl implements 
LoanDelinquencyDomainSe
             collectionData.setDelinquentDate(overdueSinceDate);
         }
         collectionData.setDelinquentAmount(outstandingAmount);
-        collectionData.setDelinquentDays(0L);
-        final long delinquentDays = overdueDays;
-        if (delinquentDays > 0) {
-            calculateDelinquentDays(effectiveDelinquencyList, businessDate, 
collectionData, delinquentDays);
-        }
+        calculateAndSetDelinquentDays(collectionData, overdueDays, 0, 
effectiveDelinquencyList, businessDate,
+                overdueSinceDateForCalculation);
         return collectionData;
 
     }
@@ -356,4 +343,18 @@ public class LoanDelinquencyDomainServiceImpl implements 
LoanDelinquencyDomainSe
         return collectionData;
     }
 
+    private void calculateAndSetDelinquentDays(CollectionData collectionData, 
long overdueDays, Integer graceDays,
+            List<LoanDelinquencyActionData> effectiveDelinquencyList, 
LocalDate businessDate, LocalDate overdueSinceDate) {
+        collectionData.setDelinquentDays(0L);
+        if (overdueDays > 0) {
+            Long pausedDays = 
delinquencyEffectivePauseHelper.getPausedDaysWithinRange(effectiveDelinquencyList,
 overdueSinceDate,
+                    businessDate);
+            if (pausedDays == null) {
+                pausedDays = 0L;
+            }
+            final long delinquentDays = overdueDays - pausedDays - graceDays;
+            collectionData.setDelinquentDays(delinquentDays > 0 ? 
delinquentDays : 0L);
+        }
+    }
+
 }
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/helper/InstallmentDelinquencyAggregatorTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/helper/InstallmentDelinquencyAggregatorTest.java
new file mode 100644
index 0000000000..16903d1eb4
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/helper/InstallmentDelinquencyAggregatorTest.java
@@ -0,0 +1,278 @@
+/**
+ * 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.delinquency.helper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.math.BigDecimal;
+import java.util.List;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import 
org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
+import 
org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for InstallmentDelinquencyAggregator.
+ *
+ * These tests cover the critical aggregation logic that groups 
installment-level delinquency data by range and sums
+ * amounts. This logic is essential for financial reporting and has zero test 
coverage before this test class was
+ * created.
+ *
+ * Test scenarios cover: - Same range aggregation (summing amounts) - 
Different range separation - Multiple installments
+ * with mixed ranges - Sorting by minimumAgeDays - Empty input handling
+ */
+class InstallmentDelinquencyAggregatorTest {
+
+    private FineractPlatformTenant testTenant;
+    private FineractPlatformTenant originalTenant;
+
+    @BeforeEach
+    void setUp() {
+        originalTenant = ThreadLocalContextUtil.getTenant();
+        testTenant = new FineractPlatformTenant(1L, "test", "Test Tenant", 
"Asia/Kolkata", null);
+        ThreadLocalContextUtil.setTenant(testTenant);
+        MoneyHelper.initializeTenantRoundingMode("test", 4);
+    }
+
+    @AfterEach
+    void tearDown() {
+        ThreadLocalContextUtil.setTenant(originalTenant);
+        MoneyHelper.clearCache();
+    }
+
+    @Test
+    void testAggregateAndSort_emptyInput_returnsEmptyList() {
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of());
+
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    void testAggregateAndSort_singleInstallment_returnsSameInstallment() {
+        LoanInstallmentDelinquencyTagData data = createTagData(1L, 1L, 
"RANGE_1", 1, 3, "250.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data));
+
+        assertThat(result).hasSize(1);
+        assertInstallmentDelinquency(result.get(0), 1L, "RANGE_1", 1, 3, 
"250.00");
+    }
+
+    @Test
+    void testAggregateAndSort_twoInstallmentsSameRange_sumsAmounts() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 3L, 
"RANGE_3", 4, 60, "250.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 3L, 
"RANGE_3", 4, 60, "500.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2));
+
+        assertThat(result).hasSize(1);
+        assertInstallmentDelinquency(result.get(0), 3L, "RANGE_3", 4, 60, 
"750.00");
+    }
+
+    @Test
+    void testAggregateAndSort_threeInstallmentsSameRange_sumsAllAmounts() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 2L, 
"RANGE_2", 2, 3, "100.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 2L, 
"RANGE_2", 2, 3, "150.00");
+        LoanInstallmentDelinquencyTagData data3 = createTagData(3L, 2L, 
"RANGE_2", 2, 3, "200.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2, data3));
+
+        assertThat(result).hasSize(1);
+        assertInstallmentDelinquency(result.get(0), 2L, "RANGE_2", 2, 3, 
"450.00");
+    }
+
+    @Test
+    void testAggregateAndSort_twoInstallmentsDifferentRanges_remainsSeparate() 
{
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 1L, 
"RANGE_1", 1, 3, "250.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 3L, 
"RANGE_3", 4, 60, "250.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2));
+
+        assertThat(result).hasSize(2);
+        assertInstallmentDelinquency(result.get(0), 1L, "RANGE_1", 1, 3, 
"250.00");
+        assertInstallmentDelinquency(result.get(1), 3L, "RANGE_3", 4, 60, 
"250.00");
+    }
+
+    @Test
+    void 
testAggregateAndSort_multipleInstallmentsMixedRanges_aggregatesAndSeparatesCorrectly()
 {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 1L, 
"RANGE_1", 1, 3, "100.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 1L, 
"RANGE_1", 1, 3, "150.00");
+        LoanInstallmentDelinquencyTagData data3 = createTagData(3L, 3L, 
"RANGE_3", 4, 60, "200.00");
+        LoanInstallmentDelinquencyTagData data4 = createTagData(4L, 3L, 
"RANGE_3", 4, 60, "300.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2, data3, 
data4));
+
+        assertThat(result).hasSize(2);
+        assertInstallmentDelinquency(result.get(0), 1L, "RANGE_1", 1, 3, 
"250.00");
+        assertInstallmentDelinquency(result.get(1), 3L, "RANGE_3", 4, 60, 
"500.00");
+    }
+
+    @Test
+    void testAggregateAndSort_sortsByMinimumAgeDaysAscending() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 3L, 
"RANGE_3", 4, 60, "250.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 1L, 
"RANGE_1", 1, 3, "250.00");
+        LoanInstallmentDelinquencyTagData data3 = createTagData(3L, 2L, 
"RANGE_2", 2, 3, "250.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2, data3));
+
+        assertThat(result).hasSize(3);
+        assertEquals(1L, result.get(0).getRangeId());
+        assertEquals(Integer.valueOf(1), result.get(0).getMinimumAgeDays());
+        assertEquals(2L, result.get(1).getRangeId());
+        assertEquals(Integer.valueOf(2), result.get(1).getMinimumAgeDays());
+        assertEquals(3L, result.get(2).getRangeId());
+        assertEquals(Integer.valueOf(4), result.get(2).getMinimumAgeDays());
+    }
+
+    @Test
+    void testAggregateAndSort_complexScenario_aggregatesSortsCorrectly() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 3L, 
"RANGE_3", 4, 60, "500.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 1L, 
"RANGE_1", 1, 3, "250.00");
+        LoanInstallmentDelinquencyTagData data3 = createTagData(3L, 3L, 
"RANGE_3", 4, 60, "250.00");
+        LoanInstallmentDelinquencyTagData data4 = createTagData(4L, 2L, 
"RANGE_2", 2, 3, "100.00");
+        LoanInstallmentDelinquencyTagData data5 = createTagData(5L, 1L, 
"RANGE_1", 1, 3, "150.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator
+                .aggregateAndSort(List.of(data1, data2, data3, data4, data5));
+
+        assertThat(result).hasSize(3);
+        assertInstallmentDelinquency(result.get(0), 1L, "RANGE_1", 1, 3, 
"400.00");
+        assertInstallmentDelinquency(result.get(1), 2L, "RANGE_2", 2, 3, 
"100.00");
+        assertInstallmentDelinquency(result.get(2), 3L, "RANGE_3", 4, 60, 
"750.00");
+    }
+
+    @Test
+    void testAggregateAndSort_nullMinimumAgeDays_treatsAsZero() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 1L, 
"NO_DELINQUENCY", null, null, "100.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 2L, 
"RANGE_1", 1, 3, "200.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2));
+
+        assertThat(result).hasSize(2);
+        assertEquals(1L, result.get(0).getRangeId());
+        assertEquals(2L, result.get(1).getRangeId());
+    }
+
+    @Test
+    void testAggregateAndSort_decimalPrecision_maintainsPrecision() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 1L, 
"RANGE_1", 1, 3, "100.12");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 1L, 
"RANGE_1", 1, 3, "200.34");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2));
+
+        assertThat(result).hasSize(1);
+        
assertThat(result.get(0).getDelinquentAmount()).isEqualByComparingTo("300.46");
+    }
+
+    @Test
+    void testAggregateAndSort_zeroAmounts_includesInResult() {
+        LoanInstallmentDelinquencyTagData data1 = createTagData(1L, 1L, 
"RANGE_1", 1, 3, "0.00");
+        LoanInstallmentDelinquencyTagData data2 = createTagData(2L, 2L, 
"RANGE_2", 2, 3, "100.00");
+
+        List<InstallmentLevelDelinquency> result = 
InstallmentDelinquencyAggregator.aggregateAndSort(List.of(data1, data2));
+
+        assertThat(result).hasSize(2);
+        assertInstallmentDelinquency(result.get(0), 1L, "RANGE_1", 1, 3, 
"0.00");
+        assertInstallmentDelinquency(result.get(1), 2L, "RANGE_2", 2, 3, 
"100.00");
+    }
+
+    private LoanInstallmentDelinquencyTagData createTagData(Long 
installmentId, Long rangeId, String classification, Integer minDays,
+            Integer maxDays, String amount) {
+        return new TestLoanInstallmentDelinquencyTagData(installmentId,
+                new TestInstallmentDelinquencyRange(rangeId, classification, 
minDays, maxDays), new BigDecimal(amount));
+    }
+
+    private void assertInstallmentDelinquency(InstallmentLevelDelinquency 
actual, Long expectedRangeId, String expectedClassification,
+            Integer expectedMinDays, Integer expectedMaxDays, String 
expectedAmount) {
+        assertNotNull(actual);
+        assertEquals(expectedRangeId, actual.getRangeId());
+        assertEquals(expectedClassification, actual.getClassification());
+        assertEquals(expectedMinDays, actual.getMinimumAgeDays());
+        assertEquals(expectedMaxDays, actual.getMaximumAgeDays());
+        
assertThat(actual.getDelinquentAmount()).isEqualByComparingTo(expectedAmount);
+    }
+
+    private static class TestLoanInstallmentDelinquencyTagData implements 
LoanInstallmentDelinquencyTagData {
+
+        private final Long id;
+        private final InstallmentDelinquencyRange range;
+        private final BigDecimal amount;
+
+        TestLoanInstallmentDelinquencyTagData(Long id, 
InstallmentDelinquencyRange range, BigDecimal amount) {
+            this.id = id;
+            this.range = range;
+            this.amount = amount;
+        }
+
+        @Override
+        public Long getId() {
+            return id;
+        }
+
+        @Override
+        public InstallmentDelinquencyRange getDelinquencyRange() {
+            return range;
+        }
+
+        @Override
+        public BigDecimal getOutstandingAmount() {
+            return amount;
+        }
+    }
+
+    private static class TestInstallmentDelinquencyRange implements 
LoanInstallmentDelinquencyTagData.InstallmentDelinquencyRange {
+
+        private final Long id;
+        private final String classification;
+        private final Integer minDays;
+        private final Integer maxDays;
+
+        TestInstallmentDelinquencyRange(Long id, String classification, 
Integer minDays, Integer maxDays) {
+            this.id = id;
+            this.classification = classification;
+            this.minDays = minDays;
+            this.maxDays = maxDays;
+        }
+
+        @Override
+        public Long getId() {
+            return id;
+        }
+
+        @Override
+        public String getClassification() {
+            return classification;
+        }
+
+        @Override
+        public Integer getMinimumAgeDays() {
+            return minDays;
+        }
+
+        @Override
+        public Integer getMaximumAgeDays() {
+            return maxDays;
+        }
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
index 593a37bd00..cd213cbf46 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
@@ -42,7 +42,10 @@ import 
org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
 import org.apache.fineract.organisation.monetary.domain.Money;
 import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
+import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
 import 
org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
+import 
org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelperImpl;
 import 
org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
 import 
org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
 import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
@@ -159,7 +162,8 @@ public class LoanDelinquencyDomainServiceTest {
         
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(repaymentScheduleInstallments.get(0));
         when(loan.getCurrency()).thenReturn(currency);
         when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
-        
when(delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList,
 businessDate)).thenReturn(0L);
+        
when(delinquencyEffectivePauseHelper.getPausedDaysWithinRange(Mockito.eq(effectiveDelinquencyList),
 Mockito.any(),
+                Mockito.eq(businessDate))).thenReturn(0L);
 
         CollectionData collectionData = 
underTest.getOverdueCollectionData(loan, effectiveDelinquencyList);
 
@@ -229,7 +233,8 @@ public class LoanDelinquencyDomainServiceTest {
         when(loan.getCurrency()).thenReturn(currency);
         when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
         when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
-        
when(delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList,
 businessDate)).thenReturn(0L);
+        
when(delinquencyEffectivePauseHelper.getPausedDaysWithinRange(Mockito.eq(effectiveDelinquencyList),
 Mockito.any(),
+                Mockito.eq(businessDate))).thenReturn(0L);
 
         LoanDelinquencyData collectionData = 
underTest.getLoanDelinquencyData(loan, effectiveDelinquencyList);
 
@@ -281,7 +286,8 @@ public class LoanDelinquencyDomainServiceTest {
         when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
         
when(loanTransactionReadService.fetchLoanTransactionsByType(loan.getId(), null, 
LoanTransactionType.CHARGEBACK))
                 .thenReturn(Arrays.asList(loanTransaction));
-        
when(delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList,
 businessDate)).thenReturn(0L);
+        
when(delinquencyEffectivePauseHelper.getPausedDaysWithinRange(Mockito.eq(effectiveDelinquencyList),
 Mockito.any(),
+                Mockito.eq(businessDate))).thenReturn(0L);
 
         LoanDelinquencyData collectionData = 
underTest.getLoanDelinquencyData(loan, effectiveDelinquencyList);
 
@@ -305,4 +311,108 @@ public class LoanDelinquencyDomainServiceTest {
 
     }
 
+    @Test
+    public void 
givenPausePeriodThenInstallmentDelinquentDaysOnlyIncludeOverlap() {
+        LocalDate overriddenBusinessDate = LocalDate.of(2022, 3, 2);
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, overriddenBusinessDate)));
+
+        LoanRepaymentScheduleInstallment installmentOne = new 
LoanRepaymentScheduleInstallment(loan, 1, LocalDate.of(2021, 12, 1),
+                LocalDate.of(2022, 1, 16), principal, zeroAmount, zeroAmount, 
zeroAmount, false, new HashSet<>(), zeroAmount);
+        installmentOne.setId(1L);
+        LoanRepaymentScheduleInstallment installmentTwo = new 
LoanRepaymentScheduleInstallment(loan, 2, LocalDate.of(2022, 1, 16),
+                LocalDate.of(2022, 1, 31), principal, zeroAmount, zeroAmount, 
zeroAmount, false, new HashSet<>(), zeroAmount);
+        installmentTwo.setId(2L);
+        LoanRepaymentScheduleInstallment installmentThree = new 
LoanRepaymentScheduleInstallment(loan, 3, LocalDate.of(2022, 1, 31),
+                LocalDate.of(2022, 2, 15), principal, zeroAmount, zeroAmount, 
zeroAmount, false, new HashSet<>(), zeroAmount);
+        installmentThree.setId(3L);
+
+        List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = 
Arrays.asList(installmentOne, installmentTwo,
+                installmentThree);
+
+        when(loanProductRelatedDetail.getGraceOnArrearsAgeing()).thenReturn(0);
+        
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
+        
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
+        
when(loanTransactionReadService.fetchLoanTransactionsByType(loan.getId(), null, 
LoanTransactionType.CHARGEBACK))
+                .thenReturn(Collections.emptyList());
+        
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(installmentThree);
+        when(loan.getCurrency()).thenReturn(currency);
+        when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
+        when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+        LoanDelinquencyAction pauseAction = new LoanDelinquencyAction(null, 
DelinquencyAction.PAUSE, LocalDate.of(2022, 1, 20),
+                LocalDate.of(2022, 1, 30));
+        pauseAction.setId(1L);
+        LoanDelinquencyActionData pauseData = new 
LoanDelinquencyActionData(pauseAction);
+        List<LoanDelinquencyActionData> effectiveDelinquencyList = 
List.of(pauseData);
+
+        LoanDelinquencyDomainServiceImpl service = new 
LoanDelinquencyDomainServiceImpl(new DelinquencyEffectivePauseHelperImpl(),
+                loanTransactionReadService);
+
+        LoanDelinquencyData collectionData = 
service.getLoanDelinquencyData(loan, effectiveDelinquencyList);
+
+        CollectionData loanCollectionData = 
collectionData.getLoanCollectionData();
+        assertEquals(35L, loanCollectionData.getDelinquentDays());
+        assertEquals(LocalDate.of(2022, 1, 16), 
loanCollectionData.getDelinquentDate());
+
+        Map<Long, CollectionData> installments = 
collectionData.getLoanInstallmentsCollectionData();
+        assertNotNull(installments);
+        assertEquals(3, installments.size());
+        assertEquals(35L, installments.get(1L).getDelinquentDays());
+        assertEquals(30L, installments.get(2L).getDelinquentDays());
+        assertEquals(15L, installments.get(3L).getDelinquentDays());
+    }
+
+    @Test
+    public void 
givenMultipleInstallmentsAndPauseThenDelinquencyDaysDistributePerInstallment() {
+        LocalDate businessDate = LocalDate.of(2022, 2, 5);
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, businessDate)));
+
+        Loan localLoan = Mockito.mock(Loan.class);
+        LoanProductRelatedDetail localDetails = 
Mockito.mock(LoanProductRelatedDetail.class);
+        MonetaryCurrency currency = new MonetaryCurrency("USD", 2, null);
+
+        LoanRepaymentScheduleInstallment installmentOne = new 
LoanRepaymentScheduleInstallment(localLoan, 1, LocalDate.of(2021, 12, 26),
+                LocalDate.of(2022, 1, 10), principal, zeroAmount, zeroAmount, 
zeroAmount, false, new HashSet<>(), zeroAmount);
+        installmentOne.setId(1L);
+        LoanRepaymentScheduleInstallment installmentTwo = new 
LoanRepaymentScheduleInstallment(localLoan, 2, LocalDate.of(2022, 1, 10),
+                LocalDate.of(2022, 1, 20), principal, zeroAmount, zeroAmount, 
zeroAmount, false, new HashSet<>(), zeroAmount);
+        installmentTwo.setId(2L);
+        LoanRepaymentScheduleInstallment installmentThree = new 
LoanRepaymentScheduleInstallment(localLoan, 3, LocalDate.of(2022, 1, 20),
+                LocalDate.of(2022, 1, 30), principal, zeroAmount, zeroAmount, 
zeroAmount, false, new HashSet<>(), zeroAmount);
+        installmentThree.setId(3L);
+
+        List<LoanRepaymentScheduleInstallment> installments = 
Arrays.asList(installmentOne, installmentTwo, installmentThree);
+
+        when(localLoan.getId()).thenReturn(42L);
+        when(localLoan.getLoanProductRelatedDetail()).thenReturn(localDetails);
+        when(localDetails.getGraceOnArrearsAgeing()).thenReturn(0);
+        
when(localLoan.getRepaymentScheduleInstallments()).thenReturn(installments);
+        
when(localLoan.getLastLoanRepaymentScheduleInstallment()).thenReturn(installmentThree);
+        when(localLoan.getCurrency()).thenReturn(currency);
+        when(localLoan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
+        when(localLoan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+        
when(loanTransactionReadService.fetchLoanTransactionsByType(localLoan.getId(), 
null, LoanTransactionType.CHARGEBACK))
+                .thenReturn(Collections.emptyList());
+
+        LoanDelinquencyAction pauseAction = new 
LoanDelinquencyAction(localLoan, DelinquencyAction.PAUSE, LocalDate.of(2022, 1, 
15),
+                LocalDate.of(2022, 1, 25));
+        List<LoanDelinquencyActionData> effectiveDelinquencyList = new 
DelinquencyEffectivePauseHelperImpl()
+                .calculateEffectiveDelinquencyList(List.of(pauseAction));
+
+        LoanDelinquencyDomainServiceImpl service = new 
LoanDelinquencyDomainServiceImpl(new DelinquencyEffectivePauseHelperImpl(),
+                loanTransactionReadService);
+
+        LoanDelinquencyData delinquencyData = 
service.getLoanDelinquencyData(localLoan, effectiveDelinquencyList);
+
+        CollectionData loanCollectionData = 
delinquencyData.getLoanCollectionData();
+        assertEquals(16L, loanCollectionData.getDelinquentDays());
+        assertEquals(LocalDate.of(2022, 1, 10), 
loanCollectionData.getDelinquentDate());
+
+        Map<Long, CollectionData> installmentData = 
delinquencyData.getLoanInstallmentsCollectionData();
+        assertEquals(3, installmentData.size());
+        assertEquals(16L, installmentData.get(1L).getDelinquentDays());
+        assertEquals(11L, installmentData.get(2L).getDelinquentDays());
+        assertEquals(6L, installmentData.get(3L).getDelinquentDays());
+    }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
index 5d45fdd202..168a029c8b 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.integrationtests;
 
+import static java.lang.Boolean.FALSE;
 import static java.lang.Boolean.TRUE;
 import static 
org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE;
 import static 
org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
@@ -27,9 +28,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -39,6 +44,7 @@ import 
org.apache.fineract.client.models.GetDelinquencyActionsResponse;
 import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdDelinquencyPausePeriod;
 import 
org.apache.fineract.client.models.GetLoansLoanIdLoanInstallmentLevelDelinquency;
+import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import org.apache.fineract.client.models.PostLoanProductsRequest;
 import org.apache.fineract.client.models.PostLoanProductsResponse;
@@ -48,6 +54,7 @@ import 
org.apache.fineract.integrationtests.common.ClientHelper;
 import 
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
 import 
org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
 import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
+import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -462,6 +469,379 @@ public class DelinquencyActionIntegrationTests extends 
BaseLoanIntegrationTest {
 
     }
 
+    private Long createLoanProductWithDelinquencyBucketNoDownPayment(boolean 
multiDisburseEnabled,
+            boolean installmentLevelDelinquencyEnabled, Integer 
graceOnArrearsAging) {
+        Integer delinquencyBucketId = 
DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, 
List.of(//
+                Pair.of(1, 3), //
+                Pair.of(4, 10), //
+                Pair.of(11, 60), //
+                Pair.of(61, null)//
+        ));
+        PostLoanProductsRequest product = 
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct();
+        product.setDelinquencyBucketId(delinquencyBucketId.longValue());
+        product.setMultiDisburseLoan(multiDisburseEnabled);
+        product.setEnableDownPayment(false);
+        product.setGraceOnArrearsAgeing(graceOnArrearsAging);
+        
product.setEnableInstallmentLevelDelinquency(installmentLevelDelinquencyEnabled);
+
+        PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+        return loanProductResponse.getResourceId();
+    }
+
+    @Test
+    public void testDelinquentDaysAndDateAfterPastDelinquencyPause() {
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("01 January 2022", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWith25PctDownPaymentAndDelinquencyBucket(true, true, false, 0);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 
January 2022", 1000.0, 2, req -> {
+                req.setLoanTermFrequency(30);
+                req.setRepaymentEvery(15);
+                req.setGraceOnArrearsAgeing(0);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 
2022");
+            loanIdHolder[0] = loanId;
+
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"20 January 2022", "30 January 2022");
+        });
+
+        runAt("02 February 2022", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+
+            assertNotNull(loanDetails.getDelinquent(), "Delinquent data should 
not be null");
+
+            Integer pastDueDays = loanDetails.getDelinquent().getPastDueDays();
+            assertNotNull(pastDueDays, "Past due days should not be null");
+            assertEquals(17, pastDueDays, "Past due days should be 17 (16 Jan 
due date to 02 Feb business date)");
+
+            Integer delinquentDays = 
loanDetails.getDelinquent().getDelinquentDays();
+            assertNotNull(delinquentDays, "Delinquent days should not be 
null");
+            assertEquals(7, delinquentDays, "Delinquent days should be 7 (17 
past due days - 10 paused days = 7)");
+
+            LocalDate delinquentDate = 
loanDetails.getDelinquent().getDelinquentDate();
+            assertNotNull(delinquentDate, "Delinquent date should not be 
null");
+            assertEquals(LocalDate.parse("16 January 2022", 
dateTimeFormatter), delinquentDate,
+                    "Delinquent date should be 16 Jan 2022 (first installment 
due date, NOT adjusted for pause)");
+
+            List<GetLoansLoanIdDelinquencyPausePeriod> pausePeriods = 
loanDetails.getDelinquent().getDelinquencyPausePeriods();
+            assertNotNull(pausePeriods);
+            assertEquals(1, pausePeriods.size());
+            assertEquals(LocalDate.parse("20 January 2022", 
dateTimeFormatter), pausePeriods.get(0).getPausePeriodStart());
+            assertEquals(LocalDate.parse("30 January 2022", 
dateTimeFormatter), pausePeriods.get(0).getPausePeriodEnd());
+            assertEquals(FALSE, pausePeriods.get(0).getActive());
+        });
+    }
+
+    @Test
+    public void 
testInstallmentLevelDelinquencyWithMultipleOverdueInstallments() {
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("01 January 2022", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWith25PctDownPaymentAndDelinquencyBucket(true, true, true, 0);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 
January 2022", 1000.0, 3, req -> {
+                req.setLoanTermFrequency(45);
+                req.setRepaymentEvery(15);
+                req.setGraceOnArrearsAgeing(0);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(100.00), "01 January 
2022");
+            loanIdHolder[0] = loanId;
+
+            businessDateHelper.updateBusinessDate(new 
BusinessDateUpdateRequest().type(BusinessDateUpdateRequest.TypeEnum.BUSINESS_DATE)
+                    .date("05 January 
2022").dateFormat(DATETIME_PATTERN).locale("en"));
+
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"20 January 2022", "30 January 2022");
+        });
+
+        runAt("02 March 2022", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+
+            assertNotNull(loanDetails.getDelinquent(), "Loan delinquent data 
should not be null");
+
+            Integer loanLevelPastDueDays = 
loanDetails.getDelinquent().getPastDueDays();
+            assertEquals(45, loanLevelPastDueDays, "Loan level past due days 
should be 45 (16 Jan to 02 Mar)");
+
+            Integer loanLevelDelinquentDays = 
loanDetails.getDelinquent().getDelinquentDays();
+            assertEquals(35, loanLevelDelinquentDays, "Loan level delinquent 
days should be 35 (45 past due days - 10 paused days = 35)");
+
+            LocalDate loanLevelDelinquentDate = 
loanDetails.getDelinquent().getDelinquentDate();
+            assertEquals(LocalDate.parse("16 January 2022", 
dateTimeFormatter), loanLevelDelinquentDate,
+                    "Loan level delinquent date should be 16 Jan 2022 (first 
installment due date)");
+
+            Map<String, BigDecimal> expectedTotals = 
calculateExpectedBucketTotals(loanDetails,
+                    LocalDate.parse("02 March 2022", dateTimeFormatter));
+            assertTrue(expectedTotals.containsKey("11-60"), "Expected 11-60 
bucket to contain delinquent installments");
+            assertInstallmentDelinquencyBuckets(loanDetails, 
LocalDate.parse("02 March 2022", dateTimeFormatter), expectedTotals);
+        });
+    }
+
+    @Test
+    public void 
testInstallmentDelinquencyWithSinglePauseAffectingMultipleInstallments() {
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("10 January 2022", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWith25PctDownPaymentAndDelinquencyBucket(true, true, true, 0);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "10 
January 2022", 1000.0, 3, req -> {
+                req.setLoanTermFrequency(30);
+                req.setRepaymentEvery(10);
+                req.setGraceOnArrearsAgeing(0);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(100.00), "10 January 
2022");
+            loanIdHolder[0] = loanId;
+
+            businessDateHelper.updateBusinessDate(new 
BusinessDateUpdateRequest().type(BusinessDateUpdateRequest.TypeEnum.BUSINESS_DATE)
+                    .date("14 January 
2022").dateFormat(DATETIME_PATTERN).locale("en"));
+
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"15 January 2022", "25 January 2022");
+        });
+
+        runAt("05 February 2022", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertNotNull(loanDetails.getDelinquent(), "Loan delinquent data 
should not be null");
+
+            List<GetLoansLoanIdLoanInstallmentLevelDelinquency> delinquencies 
= loanDetails.getDelinquent()
+                    .getInstallmentLevelDelinquency();
+            assertNotNull(delinquencies, "Installment level delinquency should 
not be null");
+
+            Map<String, BigDecimal> actualTotals = new HashMap<>();
+            for (GetLoansLoanIdLoanInstallmentLevelDelinquency delinquency : 
delinquencies) {
+                String bucketKey = 
formatBucketKey(delinquency.getMinimumAgeDays(), 
delinquency.getMaximumAgeDays());
+                actualTotals.merge(bucketKey, 
delinquency.getDelinquentAmount(), BigDecimal::add);
+            }
+
+            assertEquals(2, actualTotals.size(), "Should have 2 delinquency 
buckets");
+            assertTrue(actualTotals.containsKey("4-10"), "Should have 4-10 
bucket");
+            assertTrue(actualTotals.containsKey("11-60"), "Should have 11-60 
bucket");
+            assertEquals(0, 
BigDecimal.valueOf(25.0).compareTo(actualTotals.get("4-10")), "4-10 bucket 
should have 25.0");
+            assertEquals(0, 
BigDecimal.valueOf(25.0).compareTo(actualTotals.get("11-60")), "11-60 bucket 
should have 25.0");
+        });
+    }
+
+    @Test
+    public void 
testInstallmentDelinquencyWithMultiplePausesAffectingSameInstallment() {
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("01 January 2022", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWith25PctDownPaymentAndDelinquencyBucket(true, true, true, 0);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 
January 2022", 1000.0, 1, req -> {
+                req.setLoanTermFrequency(30);
+                req.setRepaymentEvery(30);
+                req.setGraceOnArrearsAgeing(0);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(100.00), "01 January 
2022");
+            loanIdHolder[0] = loanId;
+        });
+
+        runAt("04 February 2022", () -> {
+            Long loanId = loanIdHolder[0];
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"04 February 2022", "09 February 2022");
+        });
+
+        runAt("15 February 2022", () -> {
+            Long loanId = loanIdHolder[0];
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"15 February 2022", "20 February 2022");
+        });
+
+        runAt("01 March 2022", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertNotNull(loanDetails.getDelinquent(), "Loan delinquent data 
should not be null");
+
+            LocalDate businessDate = LocalDate.parse("01 March 2022", 
dateTimeFormatter);
+            LocalDate installmentDueDate = 
loanDetails.getDelinquent().getDelinquentDate();
+
+            Integer loanLevelPastDueDays = 
loanDetails.getDelinquent().getPastDueDays();
+            long expectedPastDueDays = 
ChronoUnit.DAYS.between(installmentDueDate, businessDate);
+            assertEquals((int) expectedPastDueDays, loanLevelPastDueDays,
+                    "Loan level past due days should match the business date 
minus the first installment due date");
+
+            Integer loanLevelDelinquentDays = 
loanDetails.getDelinquent().getDelinquentDays();
+            long expectedDelinquentDays = Math.max(expectedPastDueDays - 10, 
0);
+            assertEquals((int) expectedDelinquentDays, loanLevelDelinquentDays,
+                    "Loan level delinquent days should subtract both five-day 
pause periods from the past due days");
+
+            LocalDate loanLevelDelinquentDate = 
loanDetails.getDelinquent().getDelinquentDate();
+            assertEquals(installmentDueDate, loanLevelDelinquentDate, "Loan 
level delinquent date should equal the installment due date");
+
+            List<GetLoansLoanIdLoanInstallmentLevelDelinquency> delinquencies 
= loanDetails.getDelinquent()
+                    .getInstallmentLevelDelinquency();
+            assertNotNull(delinquencies, "Installment level delinquency should 
not be null");
+
+            Map<String, BigDecimal> actualTotals = new HashMap<>();
+            for (GetLoansLoanIdLoanInstallmentLevelDelinquency delinquency : 
delinquencies) {
+                String bucketKey = 
formatBucketKey(delinquency.getMinimumAgeDays(), 
delinquency.getMaximumAgeDays());
+                actualTotals.merge(bucketKey, 
delinquency.getDelinquentAmount(), BigDecimal::add);
+            }
+
+            assertEquals(1, actualTotals.size(), "Should have 1 delinquency 
bucket");
+            assertTrue(actualTotals.containsKey("11-60"), "Should have 11-60 
bucket");
+            assertEquals(0, 
BigDecimal.valueOf(75.0).compareTo(actualTotals.get("11-60")), "11-60 bucket 
should have 75.0");
+        });
+    }
+
+    @Test
+    public void 
testInstallmentDelinquencyWithPauseBetweenSequentialInstallments() {
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("01 January 2022", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWith25PctDownPaymentAndDelinquencyBucket(true, true, true, 0);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 
January 2022", 1000.0, 2, req -> {
+                req.setLoanTermFrequency(20);
+                req.setRepaymentEvery(10);
+                req.setGraceOnArrearsAgeing(0);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(100.00), "01 January 
2022");
+            loanIdHolder[0] = loanId;
+
+            businessDateHelper.updateBusinessDate(new 
BusinessDateUpdateRequest().type(BusinessDateUpdateRequest.TypeEnum.BUSINESS_DATE)
+                    .date("02 January 
2022").dateFormat(DATETIME_PATTERN).locale("en"));
+
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"03 January 2022", "10 January 2022");
+        });
+
+        runAt("12 January 2022", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertNotNull(loanDetails.getDelinquent(), "Loan delinquent data 
should not be null");
+
+            Map<String, BigDecimal> expectedTotals = 
calculateExpectedBucketTotals(loanDetails,
+                    LocalDate.parse("12 January 2022", dateTimeFormatter));
+            assertInstallmentDelinquencyBuckets(loanDetails, 
LocalDate.parse("12 January 2022", dateTimeFormatter), expectedTotals);
+        });
+    }
+
+    @Test
+    public void testInstallmentDelinquencyWithFourInstallmentsAndPausePeriod() 
{
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("01 January 2022", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWith25PctDownPaymentAndDelinquencyBucket(true, true, true, 0);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 
January 2022", 1000.0, 4, req -> {
+                req.setLoanTermFrequency(60);
+                req.setRepaymentEvery(15);
+                req.setGraceOnArrearsAgeing(0);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 
2022");
+            loanIdHolder[0] = loanId;
+
+            businessDateHelper.updateBusinessDate(new 
BusinessDateUpdateRequest().type(BusinessDateUpdateRequest.TypeEnum.BUSINESS_DATE)
+                    .date("01 January 
2022").dateFormat(DATETIME_PATTERN).locale("en"));
+
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"02 January 2022", "20 January 2022");
+        });
+
+        runAt("01 March 2022", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertNotNull(loanDetails.getDelinquent(), "Loan delinquent data 
should not be null");
+
+            Map<String, BigDecimal> expectedTotals = 
calculateExpectedBucketTotals(loanDetails,
+                    LocalDate.parse("01 March 2022", dateTimeFormatter));
+            assertInstallmentDelinquencyBuckets(loanDetails, 
LocalDate.parse("01 March 2022", dateTimeFormatter), expectedTotals);
+        });
+    }
+
+    @Test
+    public void testPauseUsesBusinessDateNotCOBDate() {
+        final Long[] loanIdHolder = new Long[1];
+
+        runAt("28 May 2025", () -> {
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+            Long loanProductId = 
createLoanProductWithDelinquencyBucketNoDownPayment(true, true, 3);
+            Long loanId = applyAndApproveLoan(clientId, loanProductId, "28 May 
2025", 1000.0, 7, req -> {
+                req.setLoanTermFrequency(210);
+                req.setRepaymentEvery(30);
+                req.setGraceOnArrearsAgeing(3);
+            });
+            disburseLoan(loanId, BigDecimal.valueOf(1000.00), "28 May 2025");
+            loanIdHolder[0] = loanId;
+        });
+
+        runAt("15 June 2025", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, 
"17 June 2025", "19 August 2025");
+        });
+
+        runAt("01 July 2025", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+        });
+
+        runAt("01 August 2025", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+        });
+
+        runAt("01 September 2025", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+        });
+
+        runAt("01 October 2025", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+        });
+
+        runAt("31 October 2025", () -> {
+            final InlineLoanCOBHelper inlineLoanCOBHelper = new 
InlineLoanCOBHelper(requestSpec, responseSpec);
+            Long loanId = loanIdHolder[0];
+            inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            assertNotNull(loanDetails.getDelinquent(), "Loan delinquent data 
should not be null");
+
+            Integer loanLevelPastDueDays = 
loanDetails.getDelinquent().getPastDueDays();
+            assertEquals(126, loanLevelPastDueDays,
+                    "Loan level past due days should be 126 (June 27 to Oct 
31) - First installment due June 27 (30 days after May 28)");
+
+            Integer loanLevelDelinquentDays = 
loanDetails.getDelinquent().getDelinquentDays();
+            assertEquals(70, loanLevelDelinquentDays,
+                    "Loan level delinquent days should be 70 (125 overdue days 
from June 28 to Oct 31, minus 52 paused days from June 28 to Aug 19, minus 3 
grace)");
+
+            LocalDate loanLevelDelinquentDate = 
loanDetails.getDelinquent().getDelinquentDate();
+            assertEquals(LocalDate.parse("30 June 2025", dateTimeFormatter), 
loanLevelDelinquentDate,
+                    "Loan level delinquent date should be June 30, 2025 (first 
installment due June 27 + 3 days grace)");
+
+            Map<String, BigDecimal> expectedTotals = 
calculateExpectedBucketTotals(loanDetails,
+                    LocalDate.parse("31 October 2025", dateTimeFormatter));
+            assertInstallmentDelinquencyBuckets(loanDetails, 
LocalDate.parse("31 October 2025", dateTimeFormatter), expectedTotals);
+        });
+    }
+
     @AllArgsConstructor
     public static class InstallmentDelinquencyData {
 
@@ -470,4 +850,120 @@ public class DelinquencyActionIntegrationTests extends 
BaseLoanIntegrationTest {
         BigDecimal delinquentAmount;
     }
 
+    private void assertInstallmentDelinquencyBuckets(GetLoansLoanIdResponse 
loanDetails, LocalDate businessDate,
+            Map<String, BigDecimal> expectedBucketTotals) {
+        SoftAssertions softly = new SoftAssertions();
+
+        List<GetLoansLoanIdLoanInstallmentLevelDelinquency> delinquencies = 
loanDetails.getDelinquent().getInstallmentLevelDelinquency();
+        softly.assertThat(delinquencies).as("Installment level delinquency 
should not be null").isNotNull();
+
+        Map<String, BigDecimal> calculatedTotals = 
calculateExpectedBucketTotals(loanDetails, businessDate);
+        Map<String, BigDecimal> actualTotals = new HashMap<>();
+        for (GetLoansLoanIdLoanInstallmentLevelDelinquency delinquency : 
delinquencies) {
+            String bucketKey = 
formatBucketKey(delinquency.getMinimumAgeDays(), 
delinquency.getMaximumAgeDays());
+            actualTotals.merge(bucketKey, delinquency.getDelinquentAmount(), 
BigDecimal::add);
+        }
+
+        softly.assertThat(actualTotals.keySet()).as("Unexpected delinquency 
bucket set").isEqualTo(calculatedTotals.keySet());
+
+        calculatedTotals.forEach((bucket, expectedAmount) -> {
+            BigDecimal actualAmount = actualTotals.get(bucket);
+            softly.assertThat(actualAmount).as("Missing delinquency bucket " + 
bucket).isNotNull();
+            softly.assertThat(actualAmount.setScale(2, 
RoundingMode.HALF_DOWN)).as("Unexpected delinquent amount for bucket " + bucket)
+                    .isEqualByComparingTo(expectedAmount.setScale(2, 
RoundingMode.HALF_DOWN));
+        });
+
+        if (expectedBucketTotals != null) {
+            expectedBucketTotals.forEach((bucket, amount) -> {
+                BigDecimal calculated = calculatedTotals.get(bucket);
+                softly.assertThat(calculated).as("Expected bucket " + bucket + 
" not present in calculated totals").isNotNull();
+                softly.assertThat(calculated.setScale(2, 
RoundingMode.HALF_DOWN))
+                        .as("Calculated delinquent amount did not match 
expectation for bucket " + bucket)
+                        .isEqualByComparingTo(amount.setScale(2, 
RoundingMode.HALF_DOWN));
+            });
+        }
+
+        BigDecimal loanLevelAmount = 
loanDetails.getDelinquent().getDelinquentAmount();
+        if (loanLevelAmount != null) {
+            BigDecimal actualSum = 
actualTotals.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+            softly.assertThat(actualSum.setScale(2, RoundingMode.HALF_DOWN))
+                    .as("Installment bucket totals should sum to the loan 
level delinquent amount")
+                    .isEqualByComparingTo(loanLevelAmount.setScale(2, 
RoundingMode.HALF_DOWN));
+        }
+
+        softly.assertAll();
+    }
+
+    private Map<String, BigDecimal> 
calculateExpectedBucketTotals(GetLoansLoanIdResponse loanDetails, LocalDate 
businessDate) {
+        Map<String, BigDecimal> totals = new HashMap<>();
+        List<GetLoansLoanIdDelinquencyPausePeriod> pauses = 
loanDetails.getDelinquent().getDelinquencyPausePeriods();
+
+        for (GetLoansLoanIdRepaymentPeriod period : 
loanDetails.getRepaymentSchedule().getPeriods()) {
+            if (Boolean.TRUE.equals(period.getDownPaymentPeriod())) {
+                continue;
+            }
+            LocalDate dueDate = period.getDueDate();
+            if (dueDate == null || !dueDate.isBefore(businessDate)) {
+                continue;
+            }
+            BigDecimal outstanding = period.getTotalOutstandingForPeriod();
+            if (outstanding == null || outstanding.compareTo(BigDecimal.ZERO) 
<= 0) {
+                continue;
+            }
+
+            long pastDueDays = ChronoUnit.DAYS.between(dueDate, businessDate);
+            if (pastDueDays <= 0) {
+                continue;
+            }
+
+            long pausedDays = 0L;
+            if (pauses != null) {
+                for (GetLoansLoanIdDelinquencyPausePeriod pause : pauses) {
+                    LocalDate pauseStart = pause.getPausePeriodStart();
+                    LocalDate pauseEnd = pause.getPausePeriodEnd() != null ? 
pause.getPausePeriodEnd() : businessDate;
+                    if (pauseStart == null || !pauseEnd.isAfter(pauseStart)) {
+                        continue;
+                    }
+                    LocalDate overlapStart = pauseStart.isAfter(dueDate) ? 
pauseStart : dueDate;
+                    LocalDate overlapEnd = pauseEnd.isBefore(businessDate) ? 
pauseEnd : businessDate;
+                    if (overlapEnd.isAfter(overlapStart)) {
+                        pausedDays += ChronoUnit.DAYS.between(overlapStart, 
overlapEnd);
+                    }
+                }
+            }
+
+            long delinquentDays = pastDueDays - pausedDays;
+            if (delinquentDays <= 0) {
+                continue;
+            }
+
+            String bucket = formatBucketKeyForDays(delinquentDays);
+            totals.merge(bucket, outstanding, BigDecimal::add);
+        }
+        return totals;
+    }
+
+    private String formatBucketKey(Integer minAgeDays, Integer maxAgeDays) {
+        if (minAgeDays == null) {
+            return "0";
+        }
+        if (maxAgeDays == null) {
+            return minAgeDays + "+";
+        }
+        return minAgeDays + "-" + maxAgeDays;
+    }
+
+    private String formatBucketKeyForDays(long delinquentDays) {
+        if (delinquentDays >= 1 && delinquentDays <= 3) {
+            return "1-3";
+        } else if (delinquentDays >= 4 && delinquentDays <= 10) {
+            return "4-10";
+        } else if (delinquentDays >= 11 && delinquentDays <= 60) {
+            return "11-60";
+        } else if (delinquentDays >= 61) {
+            return "61+";
+        }
+        return "0";
+    }
+
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
index 6f7669bb35..f8853b9925 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java
@@ -82,6 +82,7 @@ import 
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtens
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
 import 
org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
 import 
org.apache.fineract.integrationtests.common.products.DelinquencyRangesHelper;
+import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -1324,14 +1325,27 @@ public class DelinquencyBucketsIntegrationTest extends 
BaseLoanIntegrationTest {
             getLoansLoanIdResponse = 
loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
             loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
             GetLoansLoanIdDelinquencySummary delinquent = 
getLoansLoanIdResponse.getDelinquent();
-            assertEquals(2049.99, 
Utils.getDoubleValue(delinquent.getDelinquentAmount()));
-            assertEquals(LocalDate.of(2012, 2, 1), 
delinquent.getDelinquentDate());
-            assertEquals(31, delinquent.getDelinquentDays());
-            assertEquals(2, 
delinquent.getInstallmentLevelDelinquency().size());
-            GetLoansLoanIdLoanInstallmentLevelDelinquency 
firstInstallmentDelinquent = delinquent.getInstallmentLevelDelinquency().get(0);
-            assertEquals(BigDecimal.valueOf(1016.66), 
firstInstallmentDelinquent.getDelinquentAmount().stripTrailingZeros());
-            GetLoansLoanIdLoanInstallmentLevelDelinquency 
secondInstallmentDelinquent = 
delinquent.getInstallmentLevelDelinquency().get(1);
-            assertEquals(BigDecimal.valueOf(1033.33), 
secondInstallmentDelinquent.getDelinquentAmount().stripTrailingZeros());
+
+            SoftAssertions softly = new SoftAssertions();
+            
softly.assertThat(Utils.getDoubleValue(delinquent.getDelinquentAmount())).as("Total
 delinquent amount").isEqualTo(2049.99);
+            softly.assertThat(delinquent.getDelinquentDate()).as("Delinquent 
date").isEqualTo(LocalDate.of(2012, 2, 1));
+            softly.assertThat(delinquent.getDelinquentDays()).as("Delinquent 
days").isEqualTo(31);
+
+            // Installment-level delinquency is aggregated by range
+            // Both installments (31 days and 13 days) fall into Range 2 (4-60 
days)
+            // So we expect 1 aggregated entry with total amount 2049.99
+            
softly.assertThat(delinquent.getInstallmentLevelDelinquency()).as("Installment 
level delinquency size").hasSize(1);
+
+            if (delinquent.getInstallmentLevelDelinquency().size() >= 1) {
+                GetLoansLoanIdLoanInstallmentLevelDelinquency rangeDelinquency 
= delinquent.getInstallmentLevelDelinquency().get(0);
+                // This is the aggregated amount for all installments in Range 
2 (4-60 days)
+                
softly.assertThat(rangeDelinquency.getDelinquentAmount().stripTrailingZeros())
+                        .as("Range 2 (4-60 days) aggregated delinquent 
amount").isEqualByComparingTo(BigDecimal.valueOf(2049.99));
+                
softly.assertThat(rangeDelinquency.getMinimumAgeDays()).as("Range minimum 
days").isEqualTo(4);
+                
softly.assertThat(rangeDelinquency.getMaximumAgeDays()).as("Range maximum 
days").isEqualTo(60);
+            }
+
+            softly.assertAll();
         });
     }
 

Reply via email to