FINERACT-1971: Fix for not properly resolving delinquency range data for loan in case there's an installment delinquency
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java
new file mode 100644
index 0000000..29aa610
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java
@@ -0,0 +1,268 @@
+/**
+ * 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.service;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDelinquencyRangeChangeBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
+import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
+import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
+import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
+import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
+import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag;
+import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
+import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class DelinquencyWritePlatformServiceHelper {
+
+    private final BusinessEventNotifierService businessEventNotifierService;
+    private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
+    private final DelinquencyRangeRepository repositoryRange;
+    private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
+
+    public Map<String, Object> applyDelinquencyForLoan(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) {
+        Map<String, Object> changes = new HashMap<>();
+
+        if (overdueDays <= 0) { // No Delinquency
+            log.debug("Loan {} without delinquency range with {} days", loan.getId(), overdueDays);
+            changes = setLoanDelinquencyTag(loan, null);
+
+        } else {
+            // Sort the ranges based on the minAgeDays
+            final List<DelinquencyRange> ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges());
+
+            for (final DelinquencyRange delinquencyRange : ranges) {
+                if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket
+                    if (delinquencyRange.getMinimumAgeDays() <= overdueDays) {
+                        log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
+                                overdueDays);
+                        changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
+                        break;
+                    }
+                } else {
+                    if (delinquencyRange.getMinimumAgeDays() <= overdueDays && delinquencyRange.getMaximumAgeDays() >= overdueDays) {
+                        log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
+                                overdueDays);
+                        changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
+                        break;
+                    }
+                }
+            }
+        }
+        changes.put("overdueDays", overdueDays);
+        return changes;
+    }
+
+    public Map<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
+        Map<String, Object> changes = new HashMap<>();
+        List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = new ArrayList<>();
+        final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
+        Optional<LoanDelinquencyTagHistory> optLoanDelinquencyTag = this.loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(loan, null);
+        // The delinquencyRangeId in null means just goes out from Delinquency
+        LoanDelinquencyTagHistory loanDelinquencyTagPrev = null;
+        if (delinquencyRangeId == null) {
+            // The Loan will go out from Delinquency
+            if (optLoanDelinquencyTag.isPresent()) {
+                loanDelinquencyTagPrev = optLoanDelinquencyTag.get();
+                loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
+                loanDelinquencyTagHistory.add(loanDelinquencyTagPrev);
+                changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange());
+                // event when loan goes out of delinquency we do not calculate at
+                // installment level and remove all installment tags, so event needs to raised here.
+                if (loan.isEnableInstallmentLevelDelinquency()) {
+                    businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
+                }
+            }
+        } else {
+            if (optLoanDelinquencyTag.isPresent()) {
+                loanDelinquencyTagPrev = optLoanDelinquencyTag.get();
+            }
+            // If the Delinquency Tag has not changed
+            if (loanDelinquencyTagPrev != null && loanDelinquencyTagPrev.getDelinquencyRange().getId().equals(delinquencyRangeId)) {
+                changes.put("current", loanDelinquencyTagPrev.getDelinquencyRange());
+            } else {
+                // The previous Loan Delinquency Tag will set as Lifted
+                if (loanDelinquencyTagPrev != null) {
+                    loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
+                    loanDelinquencyTagHistory.add(loanDelinquencyTagPrev);
+                    changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange());
+                }
+
+                final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId);
+                LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null);
+                loanDelinquencyTagHistory.add(loanDelinquencyTag);
+                changes.put("current", loanDelinquencyTag.getDelinquencyRange());
+            }
+        }
+        if (loanDelinquencyTagHistory.size() > 0) {
+            this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory);
+            // if installment level delinquency is enabled event will be raised at installment level calculation, no
+            // need to raise the event here
+            if (!loan.isEnableInstallmentLevelDelinquency()) {
+                businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
+            }
+        }
+        return changes;
+    }
+
+    public List<DelinquencyRange> sortDelinquencyRangesByMinAge(List<DelinquencyRange> ranges) {
+        final Comparator<DelinquencyRange> orderByMinAge = new Comparator<DelinquencyRange>() {
+
+            @Override
+            public int compare(DelinquencyRange o1, DelinquencyRange o2) {
+                return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays());
+            }
+        };
+        Collections.sort(ranges, orderByMinAge);
+        return ranges;
+    }
+
+    public void applyDelinquencyForLoanInstallments(final Loan loan, final DelinquencyBucket delinquencyBucket,
+            final Map<Long, CollectionData> installmentsCollectionData) {
+        boolean isDelinquencyRangeChangedForAnyOfInstallment = false;
+        for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
+            if (installmentsCollectionData.containsKey(installment.getId())) {
+                boolean isDelinquencySetForInstallment = setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket,
+                        installmentsCollectionData.get(installment.getId()));
+                isDelinquencyRangeChangedForAnyOfInstallment = isDelinquencyRangeChangedForAnyOfInstallment
+                        || isDelinquencySetForInstallment;
+            }
+        }
+        // remove tags for non-existing installments that got deleted due to re-schedule
+        removeDelinquencyTagsForNonExistingInstallments(loan.getId());
+        // raise event if there is any change at installment level delinquency
+        if (isDelinquencyRangeChangedForAnyOfInstallment) {
+            businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
+        }
+
+    }
+
+    private void removeDelinquencyTagsForNonExistingInstallments(Long loanId) {
+        List<LoanInstallmentDelinquencyTag> currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository
+                .findByLoanId(loanId);
+        if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) {
+            List<Long> loanInstallmentTagsForDelete = currentLoanInstallmentDelinquencyTags.stream()
+                    .filter(tag -> tag.getInstallment() == null).map(tag -> tag.getId()).toList();
+            if (loanInstallmentTagsForDelete.size() > 0) {
+                loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTagsByIds(loanInstallmentTagsForDelete);
+            }
+        }
+    }
+
+    private boolean setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment,
+            final DelinquencyBucket delinquencyBucket, final CollectionData installmentDelinquencyData) {
+        DelinquencyRange delinquencyRangeForInstallment = getInstallmentDelinquencyRange(delinquencyBucket,
+                installmentDelinquencyData.getDelinquentDays());
+        return setDelinquencyDetailsForInstallment(loan, installment, installmentDelinquencyData, delinquencyRangeForInstallment);
+    }
+
+    private DelinquencyRange getInstallmentDelinquencyRange(final DelinquencyBucket delinquencyBucket, Long overDueDays) {
+        DelinquencyRange delinquencyRangeForInstallment = null;
+        if (overDueDays > 0) {
+            // Sort the ranges based on the minAgeDays
+            final List<DelinquencyRange> ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges());
+            for (final DelinquencyRange delinquencyRange : ranges) {
+                if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket
+                    if (delinquencyRange.getMinimumAgeDays() <= overDueDays) {
+                        delinquencyRangeForInstallment = delinquencyRange;
+                        break;
+                    }
+                } else {
+                    if (delinquencyRange.getMinimumAgeDays() <= overDueDays && delinquencyRange.getMaximumAgeDays() >= overDueDays) {
+                        delinquencyRangeForInstallment = delinquencyRange;
+                        break;
+                    }
+                }
+            }
+
+        }
+        return delinquencyRangeForInstallment;
+    }
+
+    private boolean setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment,
+            CollectionData installmentDelinquencyData, final DelinquencyRange delinquencyRangeForInstallment) {
+        List<LoanInstallmentDelinquencyTag> installmentDelinquencyTags = new ArrayList<>();
+        LocalDate delinquencyCalculationDate = DateUtils.getBusinessLocalDate();
+        boolean isDelinquencyRangeChanged = false;
+
+        LoanInstallmentDelinquencyTag previousInstallmentDelinquencyTag = loanInstallmentDelinquencyTagRepository
+                .findByLoanAndInstallment(loan, installment).orElse(null);
+
+        if (delinquencyRangeForInstallment == null) {
+            // if currentInstallmentDelinquencyTag exists and range is null, installment is out of delinquency, delete
+            // delinquency details
+            if (previousInstallmentDelinquencyTag != null) {
+                // event installment out of delinquency
+                loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag);
+                isDelinquencyRangeChanged = true;
+            }
+        } else {
+            LoanInstallmentDelinquencyTag installmentDelinquency = null;
+            if (previousInstallmentDelinquencyTag != null) {
+                if (!previousInstallmentDelinquencyTag.getDelinquencyRange().getId().equals(delinquencyRangeForInstallment.getId())) {
+                    // if current delinquency range exists and there is range change, delete previous delinquency
+                    // details and add new range details
+                    installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment,
+                            delinquencyCalculationDate, null, previousInstallmentDelinquencyTag.getFirstOverdueDate(),
+                            installmentDelinquencyData.getDelinquentAmount());
+                    loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag);
+                    // event installment delinquency range change
+                    isDelinquencyRangeChanged = true;
+                } else {
+                    previousInstallmentDelinquencyTag.setOutstandingAmount(installmentDelinquencyData.getDelinquentAmount());
+                    installmentDelinquency = previousInstallmentDelinquencyTag;
+                }
+            } else {
+                // add new range, first time delinquent
+                installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment,
+                        delinquencyCalculationDate, null, installmentDelinquencyData.getDelinquentDate(),
+                        installmentDelinquencyData.getDelinquentAmount());
+                // event installment delinquent
+                isDelinquencyRangeChanged = true;
+            }
+
+            if (installmentDelinquency != null) {
+                installmentDelinquencyTags.add(installmentDelinquency);
+            }
+
+        }
+
+        if (installmentDelinquencyTags.size() > 0) {
+            loanInstallmentDelinquencyTagRepository.saveAllAndFlush(installmentDelinquencyTags);
+        }
+        return isDelinquencyRangeChanged;
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java
index 2040299..29451c1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java
@@ -20,8 +20,6 @@
 
 import java.time.LocalDate;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -50,7 +48,6 @@
 import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
 import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
 import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
-import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag;
 import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
 import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException;
 import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
@@ -63,7 +60,6 @@
 import org.apache.fineract.portfolio.loanaccount.data.LoanDelinquencyData;
 import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
-import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
 import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
 import org.springframework.transaction.annotation.Transactional;
@@ -74,20 +70,20 @@
 
     private final DelinquencyBucketParseAndValidator dataValidatorBucket;
     private final DelinquencyRangeParseAndValidator dataValidatorRange;
-
     private final DelinquencyRangeRepository repositoryRange;
     private final DelinquencyBucketRepository repositoryBucket;
     private final DelinquencyBucketMappingsRepository repositoryBucketMappings;
     private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
     private final LoanRepositoryWrapper loanRepository;
     private final LoanProductRepository loanProductRepository;
-    private final BusinessEventNotifierService businessEventNotifierService;
     private final LoanDelinquencyDomainService loanDelinquencyDomainService;
     private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
     private final DelinquencyReadPlatformService delinquencyReadPlatformService;
     private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
     private final DelinquencyActionParseAndValidator delinquencyActionParseAndValidator;
     private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
+    private final BusinessEventNotifierService businessEventNotifierService;
+    private final DelinquencyWritePlatformServiceHelper delinquencyHelper;
 
     @Override
     public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
@@ -186,16 +182,17 @@
             final CollectionData collectionData = loanDelinquencyData.getLoanCollectionData();
             // loan installments delinquent data
             final Map<Long, CollectionData> installmentsCollectionData = loanDelinquencyData.getLoanInstallmentsCollectionData();
-            // delinquency for installments
-            if (installmentsCollectionData.size() > 0) {
-                applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
-            }
-            // delinquency for loan
-            changes = lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays());
+            log.debug("Delinquency {}", collectionData);
+
+            changes = applyDelinquencyToLoanAndInstallments(loan, delinquencyBucket, collectionData, installmentsCollectionData);
 
         }
-        return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId())
-                .withEntityExternalId(loan.getExternalId()).with(changes).build();
+        return new CommandProcessingResultBuilder() //
+                .withCommandId(command.commandId()) //
+                .withEntityId(loan.getId()) //
+                .withEntityExternalId(loan.getExternalId()) //
+                .with(changes) //
+                .build(); //
     }
 
     @Override
@@ -210,16 +207,24 @@
             final CollectionData collectionData = loanDelinquentData.getLoanCollectionData();
             // loan installments delinquent data
             final Map<Long, CollectionData> installmentsCollectionData = loanDelinquentData.getLoanInstallmentsCollectionData();
-            // delinquency for installments
-            if (installmentsCollectionData.size() > 0) {
-                applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
-            }
             log.debug("Delinquency {}", collectionData);
-            // delinquency for loan
-            lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays());
+
+            applyDelinquencyToLoanAndInstallments(loan, delinquencyBucket, collectionData, installmentsCollectionData);
         }
     }
 
+    private Map<String, Object> applyDelinquencyToLoanAndInstallments(Loan loan, DelinquencyBucket delinquencyBucket,
+            CollectionData collectionData, Map<Long, CollectionData> installmentsCollectionData) {
+        // Order is important: first calculate loan level delinquency, then the installment level
+        // delinquency for loan
+        Map<String, Object> result = delinquencyHelper.applyDelinquencyForLoan(loan, delinquencyBucket, collectionData.getDelinquentDays());
+        // delinquency for installments
+        if (!installmentsCollectionData.isEmpty()) {
+            delinquencyHelper.applyDelinquencyForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
+        }
+        return result;
+    }
+
     @Override
     @Transactional
     public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command) {
@@ -242,7 +247,8 @@
             }
         }
         businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountDelinquencyPauseChangedBusinessEvent(loan));
-        return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
+        return new CommandProcessingResultBuilder() //
+                .withCommandId(command.commandId()) //
                 .withEntityId(saved.getId()) //
                 .withOfficeId(loan.getOfficeId()) //
                 .withClientId(loan.getClientId()) //
@@ -271,7 +277,7 @@
         if (loan.isEnableInstallmentLevelDelinquency()) {
             cleanLoanInstallmentsDelinquencyTags(loan);
         }
-        setLoanDelinquencyTag(loan, null);
+        delinquencyHelper.setLoanDelinquencyTag(loan, null);
     }
 
     @Override
@@ -365,7 +371,7 @@
 
     private void validateDelinquencyRanges(List<DelinquencyRange> ranges) {
         // Sort the ranges based on the minAgeDays
-        ranges = sortDelinquencyRangesByMinAge(ranges);
+        ranges = delinquencyHelper.sortDelinquencyRangesByMinAge(ranges);
 
         DelinquencyRange prevDelinquencyRange = null;
         for (DelinquencyRange delinquencyRange : ranges) {
@@ -387,220 +393,7 @@
         }
     }
 
-    private Map<String, Object> lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) {
-        Map<String, Object> changes = new HashMap<>();
-
-        if (overdueDays <= 0) { // No Delinquency
-            log.debug("Loan {} without delinquency range with {} days", loan.getId(), overdueDays);
-            changes = setLoanDelinquencyTag(loan, null);
-
-        } else {
-            // Sort the ranges based on the minAgeDays
-            final List<DelinquencyRange> ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges());
-
-            for (final DelinquencyRange delinquencyRange : ranges) {
-                if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket
-                    if (delinquencyRange.getMinimumAgeDays() <= overdueDays) {
-                        log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
-                                overdueDays);
-                        changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
-                        break;
-                    }
-                } else {
-                    if (delinquencyRange.getMinimumAgeDays() <= overdueDays && delinquencyRange.getMaximumAgeDays() >= overdueDays) {
-                        log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
-                                overdueDays);
-                        changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
-                        break;
-                    }
-                }
-            }
-        }
-        changes.put("overdueDays", overdueDays);
-        return changes;
-    }
-
-    private Map<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
-        Map<String, Object> changes = new HashMap<>();
-        List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = new ArrayList<>();
-        final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
-        Optional<LoanDelinquencyTagHistory> optLoanDelinquencyTag = this.loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(loan, null);
-        // The delinquencyRangeId in null means just goes out from Delinquency
-        LoanDelinquencyTagHistory loanDelinquencyTagPrev = null;
-        if (delinquencyRangeId == null) {
-            // The Loan will go out from Delinquency
-            if (optLoanDelinquencyTag.isPresent()) {
-                loanDelinquencyTagPrev = optLoanDelinquencyTag.get();
-                loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
-                loanDelinquencyTagHistory.add(loanDelinquencyTagPrev);
-                changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange());
-                // event when loan goes out of delinquency we do not calculate at
-                // installment level and remove all installment tags, so event needs to raised here.
-                if (loan.isEnableInstallmentLevelDelinquency()) {
-                    businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
-                }
-            }
-        } else {
-            if (optLoanDelinquencyTag.isPresent()) {
-                loanDelinquencyTagPrev = optLoanDelinquencyTag.get();
-            }
-            // If the Delinquency Tag has not changed
-            if (loanDelinquencyTagPrev != null && loanDelinquencyTagPrev.getDelinquencyRange().getId().equals(delinquencyRangeId)) {
-                changes.put("current", loanDelinquencyTagPrev.getDelinquencyRange());
-            } else {
-                // The previous Loan Delinquency Tag will set as Lifted
-                if (loanDelinquencyTagPrev != null) {
-                    loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
-                    loanDelinquencyTagHistory.add(loanDelinquencyTagPrev);
-                    changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange());
-                }
-
-                final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId);
-                LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null);
-                loanDelinquencyTagHistory.add(loanDelinquencyTag);
-                changes.put("current", loanDelinquencyTag.getDelinquencyRange());
-            }
-        }
-        if (loanDelinquencyTagHistory.size() > 0) {
-            this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory);
-            // if installment level delinquency is enabled event will be raised at installment level calculation, no
-            // need to raise the event here
-            if (!loan.isEnableInstallmentLevelDelinquency()) {
-                businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
-            }
-        }
-        return changes;
-    }
-
-    private List<DelinquencyRange> sortDelinquencyRangesByMinAge(List<DelinquencyRange> ranges) {
-        final Comparator<DelinquencyRange> orderByMinAge = new Comparator<DelinquencyRange>() {
-
-            @Override
-            public int compare(DelinquencyRange o1, DelinquencyRange o2) {
-                return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays());
-            }
-        };
-        Collections.sort(ranges, orderByMinAge);
-        return ranges;
-    }
-
-    private void applyDelinquencyDetailsForLoanInstallments(final Loan loan, final DelinquencyBucket delinquencyBucket,
-            final Map<Long, CollectionData> installmentsCollectionData) {
-        boolean isDelinquencyRangeChangedForAnyOfInstallment = false;
-        for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
-            if (installmentsCollectionData.containsKey(installment.getId())) {
-                boolean isDelinquencySetForInstallment = setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket,
-                        installmentsCollectionData.get(installment.getId()));
-                isDelinquencyRangeChangedForAnyOfInstallment = isDelinquencyRangeChangedForAnyOfInstallment
-                        || isDelinquencySetForInstallment;
-            }
-        }
-        // remove tags for non existing installments that got deleted due to re-schedule
-        removeDelinquencyTagsForNonExistingInstallments(loan.getId());
-        // raise event if there is any change at installment level delinquency
-        if (isDelinquencyRangeChangedForAnyOfInstallment) {
-            businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan));
-        }
-
-    }
-
-    private boolean setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment,
-            final DelinquencyBucket delinquencyBucket, final CollectionData installmentDelinquencyData) {
-        DelinquencyRange delinquencyRangeForInstallment = getInstallmentDelinquencyRange(delinquencyBucket,
-                installmentDelinquencyData.getDelinquentDays());
-        return setDelinquencyDetailsForInstallment(loan, installment, installmentDelinquencyData, delinquencyRangeForInstallment);
-    }
-
-    private DelinquencyRange getInstallmentDelinquencyRange(final DelinquencyBucket delinquencyBucket, Long overDueDays) {
-        DelinquencyRange delinquencyRangeForInstallment = null;
-        if (overDueDays > 0) {
-            // Sort the ranges based on the minAgeDays
-            final List<DelinquencyRange> ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges());
-            for (final DelinquencyRange delinquencyRange : ranges) {
-                if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket
-                    if (delinquencyRange.getMinimumAgeDays() <= overDueDays) {
-                        delinquencyRangeForInstallment = delinquencyRange;
-                        break;
-                    }
-                } else {
-                    if (delinquencyRange.getMinimumAgeDays() <= overDueDays && delinquencyRange.getMaximumAgeDays() >= overDueDays) {
-                        delinquencyRangeForInstallment = delinquencyRange;
-                        break;
-                    }
-                }
-            }
-
-        }
-        return delinquencyRangeForInstallment;
-    }
-
-    private boolean setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment,
-            CollectionData installmentDelinquencyData, final DelinquencyRange delinquencyRangeForInstallment) {
-        List<LoanInstallmentDelinquencyTag> installmentDelinquencyTags = new ArrayList<>();
-        LocalDate delinquencyCalculationDate = DateUtils.getBusinessLocalDate();
-        boolean isDelinquencyRangeChanged = false;
-
-        LoanInstallmentDelinquencyTag previousInstallmentDelinquencyTag = loanInstallmentDelinquencyTagRepository
-                .findByLoanAndInstallment(loan, installment).orElse(null);
-
-        if (delinquencyRangeForInstallment == null) {
-            // if currentInstallmentDelinquencyTag exists and range is null, installment is out of delinquency, delete
-            // delinquency details
-            if (previousInstallmentDelinquencyTag != null) {
-                // event installment out of delinquency
-                loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag);
-                isDelinquencyRangeChanged = true;
-            }
-        } else {
-            LoanInstallmentDelinquencyTag installmentDelinquency = null;
-            if (previousInstallmentDelinquencyTag != null) {
-                if (!previousInstallmentDelinquencyTag.getDelinquencyRange().getId().equals(delinquencyRangeForInstallment.getId())) {
-                    // if current delinquency range exists and there is range change, delete previous delinquency
-                    // details and add new range details
-                    installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment,
-                            delinquencyCalculationDate, null, previousInstallmentDelinquencyTag.getFirstOverdueDate(),
-                            installmentDelinquencyData.getDelinquentAmount());
-                    loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag);
-                    // event installment delinquency range change
-                    isDelinquencyRangeChanged = true;
-                } else {
-                    previousInstallmentDelinquencyTag.setOutstandingAmount(installmentDelinquencyData.getDelinquentAmount());
-                    installmentDelinquency = previousInstallmentDelinquencyTag;
-                }
-            } else {
-                // add new range, first time delinquent
-                installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment,
-                        delinquencyCalculationDate, null, installmentDelinquencyData.getDelinquentDate(),
-                        installmentDelinquencyData.getDelinquentAmount());
-                // event installment delinquent
-                isDelinquencyRangeChanged = true;
-            }
-
-            if (installmentDelinquency != null) {
-                installmentDelinquencyTags.add(installmentDelinquency);
-            }
-
-        }
-
-        if (installmentDelinquencyTags.size() > 0) {
-            loanInstallmentDelinquencyTagRepository.saveAllAndFlush(installmentDelinquencyTags);
-        }
-        return isDelinquencyRangeChanged;
-    }
-
     private void cleanLoanInstallmentsDelinquencyTags(Loan loan) {
         loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTags(loan.getId());
     }
-
-    private void removeDelinquencyTagsForNonExistingInstallments(Long loanId) {
-        List<LoanInstallmentDelinquencyTag> currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository
-                .findByLoanId(loanId);
-        if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) {
-            List<Long> loanInstallmentTagsForDelete = currentLoanInstallmentDelinquencyTags.stream()
-                    .filter(tag -> tag.getInstallment() == null).map(tag -> tag.getId()).toList();
-            if (loanInstallmentTagsForDelete.size() > 0) {
-                loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTagsByIds(loanInstallmentTagsForDelete);
-            }
-        }
-    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java
index 897e62b..7848ec7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java
@@ -33,6 +33,7 @@
 import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
 import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformServiceImpl;
 import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
+import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceHelper;
 import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
 import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
 import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
@@ -76,11 +77,13 @@
             LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository,
             DelinquencyReadPlatformService delinquencyReadPlatformService, LoanDelinquencyActionRepository loanDelinquencyActionRepository,
             DelinquencyActionParseAndValidator delinquencyActionParseAndValidator,
-            DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper) {
+            DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper,
+            DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper) {
         return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
-                repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
-                loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService,
-                loanDelinquencyActionRepository, delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper);
+                repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, loanDelinquencyDomainService,
+                loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, loanDelinquencyActionRepository,
+                delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper, businessEventNotifierService,
+                delinquencyWritePlatformServiceHelper);
     }
 
     @Bean
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java
index 7bd0c22..83fc041 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java
@@ -24,6 +24,8 @@
 import static org.mockito.ArgumentMatchers.anyIterable;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -62,6 +64,7 @@
 import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
 import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
 import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
+import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceHelper;
 import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
 import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
 import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
@@ -81,7 +84,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -120,7 +123,8 @@
     @Mock
     private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
 
-    @InjectMocks
+    private DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper;
+
     private DelinquencyWritePlatformServiceImpl underTest;
 
     @BeforeEach
@@ -129,6 +133,14 @@
         ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
         ThreadLocalContextUtil
                 .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault()))));
+
+        delinquencyWritePlatformServiceHelper = Mockito.spy(new DelinquencyWritePlatformServiceHelper(businessEventNotifierService,
+                loanDelinquencyTagRepository, repositoryRange, loanInstallmentDelinquencyTagRepository));
+        underTest = new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
+                repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, loanDelinquencyDomainService,
+                loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, loanDelinquencyActionRepository,
+                delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper, businessEventNotifierService,
+                delinquencyWritePlatformServiceHelper);
     }
 
     @AfterEach
@@ -181,6 +193,68 @@
     }
 
     @Test
+    public void test_ApplyDelinquencyTagToLoan_ExecutesDelinquencyApplication_InTheRightOrder() {
+        // given
+        final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
+        Loan loanForProcessing = Mockito.mock(Loan.class);
+        LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
+        DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
+        range1.setId(1L);
+        DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
+        range2.setId(2L);
+        List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
+        DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
+        delinquencyBucket.setRanges(listDelinquencyRanges);
+
+        final Long daysDiff = 2L;
+        final LocalDate fromDate = DateUtils.getBusinessLocalDate().minusMonths(1).minusDays(daysDiff);
+        final LocalDate dueDate = DateUtils.getBusinessLocalDate().minusDays(daysDiff);
+        final BigDecimal installmentPrincipalAmount = BigDecimal.valueOf(100);
+        final BigDecimal zeroAmount = BigDecimal.ZERO;
+
+        LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, fromDate, dueDate,
+                installmentPrincipalAmount, zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount);
+        installment.setId(1L);
+
+        List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = Arrays.asList(installment);
+
+        LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
+        LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
+                loanForProcessing);
+        CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
+                null, null, null, null);
+
+        CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
+                installmentPrincipalAmount, null, null, null, null, null, null);
+
+        Map<Long, CollectionData> installmentsCollection = new HashMap<>();
+        installmentsCollection.put(1L, installmentCollectionData);
+
+        LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection);
+
+        when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
+        when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
+        when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
+        when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
+        when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true);
+        when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
+        when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing, effectiveDelinquencyList))
+                .thenReturn(loanDelinquencyData);
+        when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0)))
+                .thenReturn(Optional.empty());
+
+        // when
+        underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData, effectiveDelinquencyList);
+
+        // then
+        InOrder inOrder = inOrder(delinquencyWritePlatformServiceHelper);
+        inOrder.verify(delinquencyWritePlatformServiceHelper).applyDelinquencyForLoan(eq(loanForProcessing), eq(delinquencyBucket),
+                anyLong());
+        inOrder.verify(delinquencyWritePlatformServiceHelper).applyDelinquencyForLoanInstallments(eq(loanForProcessing),
+                eq(delinquencyBucket), eq(installmentsCollection));
+    }
+
+    @Test
     public void givenLoanAccountWithDelinquencyBucketWhenNoRangeChangeThenNoEventIsRaised() {
         // given
         final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();