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

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


The following commit(s) were added to refs/heads/develop by this push:
     new 64b453e9ab FINERACT-2181: Introduce Loop guard to avoid endless loops
64b453e9ab is described below

commit 64b453e9ab1a45ca2761d25a976f6eeca956d059
Author: Adam Saghy <[email protected]>
AuthorDate: Fri Apr 4 22:19:03 2025 +0200

    FINERACT-2181: Introduce Loop guard to avoid endless loops
---
 .../java/org/apache/fineract/util/LoopContext.java |  21 +
 .../java/org/apache/fineract/util/LoopGuard.java   |  56 ++
 .../org/apache/fineract/util/LoopGuardTest.java    | 134 +++
 ...dvancedPaymentScheduleTransactionProcessor.java | 965 ++++++++++++---------
 4 files changed, 767 insertions(+), 409 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/util/LoopContext.java 
b/fineract-core/src/main/java/org/apache/fineract/util/LoopContext.java
new file mode 100644
index 0000000000..4636bd4182
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/util/LoopContext.java
@@ -0,0 +1,21 @@
+/**
+ * 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.util;
+
+public interface LoopContext {}
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/util/LoopGuard.java 
b/fineract-core/src/main/java/org/apache/fineract/util/LoopGuard.java
new file mode 100644
index 0000000000..522aa3d1b1
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/util/LoopGuard.java
@@ -0,0 +1,56 @@
+/**
+ * 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.util;
+
+import java.util.function.Predicate;
+
+/**
+ * Loop Guard is a utility solution to avoid endless loops
+ *
+ * Example: LoopGuard.runSafeDoWhileLoop(500, () -> { // loop body }, () -> 
conditions() );
+ */
+public final class LoopGuard {
+
+    private LoopGuard() {}
+
+    public interface LoopBody<T extends LoopContext> {
+
+        void execute(T context);
+    }
+
+    public static <T extends LoopContext> void runSafeDoWhileLoop(int 
maxIterations, T context, Predicate<T> condition, LoopBody<T> body) {
+        int count = 0;
+        do {
+            if (++count > maxIterations) {
+                throw new IllegalStateException("Loop exceeded " + 
maxIterations + " iterations. Possible infinite loop.");
+            }
+            body.execute(context);
+        } while (condition.test(context));
+    }
+
+    public static <T extends LoopContext> void runSafeWhileLoop(int 
maxIterations, T context, Predicate<T> condition, LoopBody<T> body) {
+        int count = 0;
+        while (condition.test(context)) {
+            if (++count > maxIterations) {
+                throw new IllegalStateException("Loop exceeded " + 
maxIterations + " iterations. Possible infinite loop.");
+            }
+            body.execute(context);
+        }
+    }
+}
diff --git 
a/fineract-core/src/test/java/org/apache/fineract/util/LoopGuardTest.java 
b/fineract-core/src/test/java/org/apache/fineract/util/LoopGuardTest.java
new file mode 100644
index 0000000000..ab07946524
--- /dev/null
+++ b/fineract-core/src/test/java/org/apache/fineract/util/LoopGuardTest.java
@@ -0,0 +1,134 @@
+/**
+ * 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.util;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+public class LoopGuardTest {
+
+    static class TestContext implements LoopContext {
+
+        int iteration;
+    }
+
+    @ParameterizedTest
+    // target value, max iterations
+    @CsvSource({ "3, 5", //
+            "5, 6", //
+            "2, 10", //
+            "2, 2", //
+    })
+    void testSafeDoWhileLoopExecutesCorrectly(int targetValue, int 
maxIterations) {
+        TestContext context = new TestContext();
+
+        Predicate<TestContext> condition = ctx -> ctx.iteration < targetValue;
+        LoopGuard.LoopBody<TestContext> body = ctx -> ctx.iteration++;
+
+        LoopGuard.runSafeDoWhileLoop(maxIterations, context, condition, body);
+
+        Assertions.assertEquals(targetValue, context.iteration);
+    }
+
+    @ParameterizedTest
+    // target value, max iterations
+    @CsvSource({ "2, 5", //
+            "4, 5", //
+            "6, 10", //
+            "2, 2" //
+    })
+    void testSafeWhileLoopExecutesCorrectly(int targetValue, int 
maxIterations) {
+        TestContext context = new TestContext();
+
+        Predicate<TestContext> condition = ctx -> ctx.iteration < targetValue;
+        LoopGuard.LoopBody<TestContext> body = ctx -> ctx.iteration++;
+
+        LoopGuard.runSafeWhileLoop(maxIterations, context, condition, body);
+
+        Assertions.assertEquals(targetValue, context.iteration);
+    }
+
+    @ParameterizedTest
+    // max iterations
+    @CsvSource({ "10", //
+            "5", //
+            "1", //
+            "0", //
+            "-1" //
+    })
+    void testSafeDoWhileLoopThrowsOnExceedingMaxIterations(int maxIterations) {
+        TestContext context = new TestContext();
+
+        Predicate<TestContext> condition = ctx -> true; // infinite
+        LoopGuard.LoopBody<TestContext> body = ctx -> ctx.iteration++;
+
+        IllegalStateException exception = 
Assertions.assertThrows(IllegalStateException.class,
+                () -> LoopGuard.runSafeDoWhileLoop(maxIterations, context, 
condition, body));
+
+        Assertions.assertTrue(exception.getMessage().contains("Loop exceeded " 
+ maxIterations));
+    }
+
+    @ParameterizedTest
+    // max iterations
+    @CsvSource({ "10", //
+            "3", //
+            "2" //
+    })
+    void testSafeWhileLoopThrowsOnExceedingMaxIterations(int maxIterations) {
+        TestContext context = new TestContext();
+
+        Predicate<TestContext> condition = ctx -> true; // infinite
+        LoopGuard.LoopBody<TestContext> body = ctx -> ctx.iteration++;
+
+        IllegalStateException exception = 
Assertions.assertThrows(IllegalStateException.class,
+                () -> LoopGuard.runSafeWhileLoop(maxIterations, context, 
condition, body));
+
+        Assertions.assertTrue(exception.getMessage().contains("Loop exceeded " 
+ maxIterations));
+    }
+
+    @Test
+    void testSafeDoWhileLoopRunsAtLeastOnce() {
+        TestContext context = new TestContext();
+        AtomicInteger counter = new AtomicInteger();
+
+        Predicate<TestContext> condition = ctx -> false;
+        LoopGuard.LoopBody<TestContext> body = ctx -> 
counter.incrementAndGet();
+
+        LoopGuard.runSafeDoWhileLoop(3, context, condition, body);
+
+        Assertions.assertEquals(1, counter.get(), "Do-while loop should 
execute at least once");
+    }
+
+    @Test
+    void testSafeWhileLoopSkipsIfConditionFalseInitially() {
+        TestContext context = new TestContext();
+        AtomicInteger counter = new AtomicInteger();
+
+        Predicate<TestContext> condition = ctx -> false;
+        LoopGuard.LoopBody<TestContext> body = ctx -> 
counter.incrementAndGet();
+
+        LoopGuard.runSafeWhileLoop(3, context, condition, body);
+
+        Assertions.assertEquals(0, counter.get(), "While loop should skip 
execution if condition is false");
+    }
+}
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 2feff9bd7a..c7ca894162 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -103,6 +103,8 @@ import 
org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocat
 import 
org.apache.fineract.portfolio.loanproduct.domain.LoanPreCloseInterestCalculationStrategy;
 import 
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
 import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.apache.fineract.util.LoopContext;
+import org.apache.fineract.util.LoopGuard;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -858,7 +860,6 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         List<LoanTransaction> remainingTransactions = new 
ArrayList<>(overpaidTransactions);
         MonetaryCurrency currency = ctx.getCurrency();
         MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder();
-        Set<LoanCharge> charges = ctx.getCharges();
         Money zero = Money.zero(currency);
         for (LoanTransaction transaction : overpaidTransactions) {
             Money overpayment = transaction.getOverPaymentPortion(currency);
@@ -880,7 +881,7 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
             List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings = new ArrayList<>();
             Balances balances = new Balances(zero, zero, zero, zero);
 
-            Money unprocessed = processPeriods(processTransaction, 
processAmount, charges, transactionMappings, balances, ctx);
+            Money unprocessed = processPeriods(processTransaction, 
processAmount, transactionMappings, balances, ctx);
 
             processTransaction.setOverPayments(MathUtil.plus(overpayment, 
unprocessed));
             overpaymentHolder.setMoneyObject(MathUtil.plus(ctxOverpayment, 
unprocessed));
@@ -1139,8 +1140,8 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
             Money zero = Money.zero(currency);
             Balances balances = new Balances(zero, zero, zero, zero);
             LoanPaymentAllocationRule defaultAllocationRule = 
getDefaultAllocationRule(loanTransaction.getLoan());
-            Money transactionAmountUnprocessed = 
processPeriods(loanTransaction, overpayment, defaultAllocationRule, Set.of(),
-                    transactionMappings, balances, transactionCtx);
+            Money transactionAmountUnprocessed = 
processPeriods(loanTransaction, overpayment, defaultAllocationRule, 
transactionMappings,
+                    balances, transactionCtx);
 
             overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
             
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
@@ -1609,164 +1610,198 @@ public class 
AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
     private Money refundTransactionHorizontally(LoanTransaction 
loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
             List<PaymentAllocationType> paymentAllocationTypes, 
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
             List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances) {
-        MonetaryCurrency currency = ctx.getCurrency();
-        Money zero = Money.zero(currency);
-        List<LoanRepaymentScheduleInstallment> installments = 
ctx.getInstallments();
-        Set<LoanCharge> charges = ctx.getCharges();
-        Money refundedPortion;
-        outerLoop: do {
-            LoanRepaymentScheduleInstallment latestPastDueInstallment = 
getLatestPastDueInstallmentForRefund(loanTransaction, currency,
-                    installments, zero);
-            LoanRepaymentScheduleInstallment dueInstallment = 
getDueInstallmentForRefund(loanTransaction, currency, installments, zero);
-
-            List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = 
getFutureInstallmentsForRefund(loanTransaction, currency,
-                    installments, futureInstallmentAllocationRule, zero);
-
-            int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
-            for (PaymentAllocationType paymentAllocationType : 
paymentAllocationTypes) {
-                switch (paymentAllocationType.getDueType()) {
-                    case PAST_DUE -> {
-                        if (latestPastDueInstallment != null) {
-                            Set<LoanCharge> oldestPastDueInstallmentCharges = 
getLoanChargesOfInstallment(charges, latestPastDueInstallment,
-                                    firstNormalInstallmentNumber);
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
latestPastDueInstallment, currency);
-                            refundedPortion = 
processPaymentAllocation(paymentAllocationType, latestPastDueInstallment, 
loanTransaction,
-                                    transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping,
-                                    oldestPastDueInstallmentCharges, balances, 
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(refundedPortion);
-                        } else {
-                            break outerLoop;
-                        }
-                    }
-                    case DUE -> {
-                        if (dueInstallment != null) {
-                            Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(charges, dueInstallment,
-                                    firstNormalInstallmentNumber);
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
dueInstallment, currency);
-                            refundedPortion = 
processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
-                                    transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
-                                    balances, 
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(refundedPortion);
-                        } else {
-                            break outerLoop;
-                        }
-                    }
-                    case IN_ADVANCE -> {
-                        int numberOfInstallments = 
inAdvanceInstallments.size();
-                        if (numberOfInstallments > 0) {
-                            Money evenPortion = 
transactionAmountUnprocessed.dividedBy(numberOfInstallments, 
MoneyHelper.getMathContext());
-                            Money balanceAdjustment = 
transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
-                            for (LoanRepaymentScheduleInstallment 
inAdvanceInstallment : inAdvanceInstallments) {
-                                Set<LoanCharge> inAdvanceInstallmentCharges = 
getLoanChargesOfInstallment(charges, inAdvanceInstallment,
-                                        firstNormalInstallmentNumber);
-                                if 
(inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 
1))) {
-                                    evenPortion = 
evenPortion.add(balanceAdjustment);
+        HorizontalPaymentAllocationContext paymentAllocationContext = new 
HorizontalPaymentAllocationContext(ctx, loanTransaction,
+                paymentAllocationTypes, futureInstallmentAllocationRule, 
transactionMappings, balances);
+        
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
+
+        
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size()
 * 100, //
+                paymentAllocationContext, //
+                (HorizontalPaymentAllocationContext context) -> 
!context.isExitCondition()
+                        && context.getCtx().getInstallments().stream()
+                                .anyMatch(installment -> 
installment.getTotalPaid(context.getCtx().getCurrency()).isGreaterThanZero())
+                        && 
context.getTransactionAmountUnprocessed().isGreaterThanZero(), //
+                context -> {
+                    LoanRepaymentScheduleInstallment latestPastDueInstallment 
= getLatestPastDueInstallmentForRefund(
+                            context.getLoanTransaction(), 
context.getCtx().getCurrency(), context.getCtx().getInstallments());
+                    LoanRepaymentScheduleInstallment dueInstallment = 
getDueInstallmentForRefund(context.getLoanTransaction(),
+                            context.getCtx().getCurrency(), 
context.getCtx().getInstallments());
+
+                    List<LoanRepaymentScheduleInstallment> 
inAdvanceInstallments = getFutureInstallmentsForRefund(
+                            context.getLoanTransaction(), 
context.getCtx().getCurrency(), context.getCtx().getInstallments(),
+                            context.getFutureInstallmentAllocationRule());
+
+                    int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper
+                            
.fetchFirstNormalInstallmentNumber(context.getCtx().getInstallments());
+                    for (PaymentAllocationType paymentAllocationType : 
context.getPaymentAllocationTypes()) {
+                        switch (paymentAllocationType.getDueType()) {
+                            case PAST_DUE -> {
+                                if (latestPastDueInstallment != null) {
+                                    Set<LoanCharge> 
oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
+                                            context.getCtx().getCharges(), 
latestPastDueInstallment, firstNormalInstallmentNumber);
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), latestPastDueInstallment,
+                                            context.getCtx().getCurrency());
+                                    
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, 
latestPastDueInstallment,
+                                            context.getLoanTransaction(), 
context.getTransactionAmountUnprocessed(),
+                                            
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
+                                            context.getBalances(), 
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                } else {
+                                    context.setExitCondition(true);
+                                }
+                            }
+                            case DUE -> {
+                                if (dueInstallment != null) {
+                                    Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(context.getCtx().getCharges(),
+                                            dueInstallment, 
firstNormalInstallmentNumber);
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), dueInstallment,
+                                            context.getCtx().getCurrency());
+                                    
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, 
dueInstallment,
+                                            context.getLoanTransaction(), 
context.getTransactionAmountUnprocessed(),
+                                            
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, 
context.getBalances(),
+                                            
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                } else {
+                                    context.setExitCondition(true);
+                                }
+                            }
+                            case IN_ADVANCE -> {
+                                int numberOfInstallments = 
inAdvanceInstallments.size();
+                                if (numberOfInstallments > 0) {
+                                    Money evenPortion = 
context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
+                                            MoneyHelper.getMathContext());
+                                    Money balanceAdjustment = 
context.getTransactionAmountUnprocessed()
+                                            
.minus(evenPortion.multipliedBy(numberOfInstallments));
+                                    for (LoanRepaymentScheduleInstallment 
inAdvanceInstallment : inAdvanceInstallments) {
+                                        Set<LoanCharge> 
inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
+                                                context.getCtx().getCharges(), 
inAdvanceInstallment, firstNormalInstallmentNumber);
+                                        if 
(inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 
1))) {
+                                            evenPortion = 
evenPortion.add(balanceAdjustment);
+                                        }
+                                        
LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                                
context.getTransactionMappings(), context.getLoanTransaction(), 
inAdvanceInstallment,
+                                                
context.getCtx().getCurrency());
+                                        
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, 
inAdvanceInstallment,
+                                                context.getLoanTransaction(), 
evenPortion, loanTransactionToRepaymentScheduleMapping,
+                                                inAdvanceInstallmentCharges, 
context.getBalances(),
+                                                
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
+                                        
context.setTransactionAmountUnprocessed(
+                                                
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                    }
+                                } else {
+                                    context.setExitCondition(true);
                                 }
-                                LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                        transactionMappings, loanTransaction, 
inAdvanceInstallment, currency);
-                                refundedPortion = 
processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, 
loanTransaction,
-                                        evenPortion, 
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, 
balances,
-                                        
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
-                                transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(refundedPortion);
                             }
-                        } else {
-                            break outerLoop;
                         }
                     }
-                }
-            }
-        } while (installments.stream().anyMatch(installment -> 
installment.getTotalPaid(currency).isGreaterThan(zero))
-                && transactionAmountUnprocessed.isGreaterThanZero());
-        return transactionAmountUnprocessed;
+                });
+        return paymentAllocationContext.getTransactionAmountUnprocessed();
     }
 
     private Money refundTransactionVertically(LoanTransaction loanTransaction, 
TransactionCtx ctx,
             List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Money transactionAmountUnprocessed,
             FutureInstallmentAllocationRule futureInstallmentAllocationRule, 
Balances balances,
             PaymentAllocationType paymentAllocationType) {
-        MonetaryCurrency currency = ctx.getCurrency();
-        Money zero = Money.zero(currency);
-        Money refundedPortion = zero;
-        List<LoanRepaymentScheduleInstallment> installments = 
ctx.getInstallments();
-        Set<LoanCharge> charges = ctx.getCharges();
-        LoanRepaymentScheduleInstallment currentInstallment = null;
-        int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
-        do {
-            switch (paymentAllocationType.getDueType()) {
-                case PAST_DUE -> {
-                    currentInstallment = 
getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, 
zero);
-                    if (currentInstallment != null) {
-                        Set<LoanCharge> oldestPastDueInstallmentCharges = 
getLoanChargesOfInstallment(charges, currentInstallment,
-                                firstNormalInstallmentNumber);
-                        LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                transactionMappings, loanTransaction, 
currentInstallment, currency);
-                        refundedPortion = 
processPaymentAllocation(paymentAllocationType, currentInstallment, 
loanTransaction,
-                                transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
-                                balances, 
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
-                        transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(refundedPortion);
-                    }
-                }
-                case DUE -> {
-                    currentInstallment = 
getDueInstallmentForRefund(loanTransaction, currency, installments, zero);
-                    if (currentInstallment != null) {
-                        Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(charges, currentInstallment,
-                                firstNormalInstallmentNumber);
-                        LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                transactionMappings, loanTransaction, 
currentInstallment, currency);
-                        refundedPortion = 
processPaymentAllocation(paymentAllocationType, currentInstallment, 
loanTransaction,
-                                transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, balances,
-                                
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
-                        transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(refundedPortion);
-                    }
-                }
-                case IN_ADVANCE -> {
-                    List<LoanRepaymentScheduleInstallment> currentInstallments 
= getFutureInstallmentsForRefund(loanTransaction, currency,
-                            installments, futureInstallmentAllocationRule, 
zero);
-                    int numberOfInstallments = currentInstallments.size();
-                    refundedPortion = zero;
-                    if (numberOfInstallments > 0) {
-                        Money evenPortion = 
transactionAmountUnprocessed.dividedBy(numberOfInstallments, 
MoneyHelper.getMathContext());
-                        Money balanceAdjustment = 
transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
-                        for (LoanRepaymentScheduleInstallment 
internalCurrentInstallment : currentInstallments) {
-                            currentInstallment = internalCurrentInstallment;
-                            Set<LoanCharge> inAdvanceInstallmentCharges = 
getLoanChargesOfInstallment(charges, currentInstallment,
-                                    firstNormalInstallmentNumber);
-                            if 
(internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments 
- 1))) {
-                                evenPortion = 
evenPortion.add(balanceAdjustment);
+        VerticalPaymentAllocationContext paymentAllocationContext = new 
VerticalPaymentAllocationContext(ctx, loanTransaction,
+                futureInstallmentAllocationRule, transactionMappings, 
balances);
+        
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
+        
paymentAllocationContext.setPaymentAllocationType(paymentAllocationType);
+        
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size()
 * 100, //
+                paymentAllocationContext, //
+                (VerticalPaymentAllocationContext context) -> 
context.getInstallment() != null
+                        && 
context.getTransactionAmountUnprocessed().isGreaterThanZero()
+                        && context.getAllocatedAmount().isGreaterThanZero(), //
+                context -> {
+                    switch (context.getPaymentAllocationType().getDueType()) {
+                        case PAST_DUE -> {
+                            
context.setInstallment(getLatestPastDueInstallmentForRefund(context.getLoanTransaction(),
+                                    context.getCtx().getCurrency(), 
context.getCtx().getInstallments()));
+                            if (context.getInstallment() != null) {
+                                Set<LoanCharge> 
oldestPastDueInstallmentCharges = 
getLoanChargesOfInstallment(context.getCtx().getCharges(),
+                                        context.getInstallment(), 
context.getFirstNormalInstallmentNumber());
+                                LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                        context.getTransactionMappings(), 
context.getLoanTransaction(), context.getInstallment(),
+                                        context.getCtx().getCurrency());
+                                
context.setAllocatedAmount(processPaymentAllocation(context.getPaymentAllocationType(),
+                                        context.getInstallment(), 
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+                                        
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges, 
context.getBalances(),
+                                        
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
+                                context.setTransactionAmountUnprocessed(
+                                        
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
                             }
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
currentInstallment, currency);
-                            Money internalUnpaidPortion = 
processPaymentAllocation(paymentAllocationType, currentInstallment,
-                                    loanTransaction, evenPortion, 
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
-                                    balances, 
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
-                            if (internalUnpaidPortion.isGreaterThanZero()) {
-                                refundedPortion = internalUnpaidPortion;
+                        }
+                        case DUE -> {
+                            
context.setInstallment(getDueInstallmentForRefund(context.getLoanTransaction(), 
context.getCtx().getCurrency(),
+                                    context.getCtx().getInstallments()));
+                            if (context.getInstallment() != null) {
+                                Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(context.getCtx().getCharges(),
+                                        context.getInstallment(), 
context.getFirstNormalInstallmentNumber());
+                                LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                        context.getTransactionMappings(), 
context.getLoanTransaction(), context.getInstallment(),
+                                        context.getCtx().getCurrency());
+                                
context.setAllocatedAmount(processPaymentAllocation(context.getPaymentAllocationType(),
+                                        context.getInstallment(), 
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+                                        
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, 
context.getBalances(),
+                                        
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
+                                context.setTransactionAmountUnprocessed(
+                                        
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                            }
+                        }
+                        case IN_ADVANCE -> {
+                            List<LoanRepaymentScheduleInstallment> 
currentInstallments = getFutureInstallmentsForRefund(
+                                    context.getLoanTransaction(), 
context.getCtx().getCurrency(), context.getCtx().getInstallments(),
+                                    
context.getFutureInstallmentAllocationRule());
+                            int numberOfInstallments = 
currentInstallments.size();
+                            
context.setAllocatedAmount(Money.zero(context.getCtx().getCurrency()));
+                            if (numberOfInstallments > 0) {
+                                Money evenPortion = 
context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
+                                        MoneyHelper.getMathContext());
+                                Money balanceAdjustment = 
context.getTransactionAmountUnprocessed()
+                                        
.minus(evenPortion.multipliedBy(numberOfInstallments));
+                                for (LoanRepaymentScheduleInstallment 
internalCurrentInstallment : currentInstallments) {
+                                    
context.setInstallment(internalCurrentInstallment);
+                                    Set<LoanCharge> 
inAdvanceInstallmentCharges = 
getLoanChargesOfInstallment(context.getCtx().getCharges(),
+                                            context.getInstallment(), 
context.getFirstNormalInstallmentNumber());
+                                    if 
(internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments 
- 1))) {
+                                        evenPortion = 
evenPortion.add(balanceAdjustment);
+                                    }
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), context.getInstallment(),
+                                            context.getCtx().getCurrency());
+                                    Money internalUnpaidPortion = 
processPaymentAllocation(context.getPaymentAllocationType(),
+                                            context.getInstallment(), 
context.getLoanTransaction(), evenPortion,
+                                            
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, 
context.getBalances(),
+                                            
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
+                                    if 
(internalUnpaidPortion.isGreaterThanZero()) {
+                                        
context.setAllocatedAmount(internalUnpaidPortion);
+                                    }
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(internalUnpaidPortion));
+                                }
+                            } else {
+                                context.setInstallment(null);
                             }
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(internalUnpaidPortion);
                         }
-                    } else {
-                        currentInstallment = null;
                     }
-                }
-            }
-        } while (currentInstallment != null && 
transactionAmountUnprocessed.isGreaterThanZero() && 
refundedPortion.isGreaterThanZero());
-        return transactionAmountUnprocessed;
+                });
+        return paymentAllocationContext.getTransactionAmountUnprocessed();
     }
 
     @Nullable
     private static LoanRepaymentScheduleInstallment 
getDueInstallmentForRefund(LoanTransaction loanTransaction, MonetaryCurrency 
currency,
-            List<LoanRepaymentScheduleInstallment> installments, Money zero) {
-        return installments.stream().filter(installment -> 
installment.getTotalPaid(currency).isGreaterThan(zero))
+            List<LoanRepaymentScheduleInstallment> installments) {
+        return installments.stream().filter(installment -> 
installment.getTotalPaid(currency).isGreaterThanZero())
                 .filter(installment -> 
loanTransaction.isOn(installment.getDueDate()))
                 
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
     }
 
     @Nullable
     private static LoanRepaymentScheduleInstallment 
getLatestPastDueInstallmentForRefund(LoanTransaction loanTransaction,
-            MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> 
installments, Money zero) {
-        return installments.stream().filter(installment -> 
installment.getTotalPaid(currency).isGreaterThan(zero))
+            MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> 
installments) {
+        return installments.stream().filter(installment -> 
installment.getTotalPaid(currency).isGreaterThanZero())
                 .filter(e -> loanTransaction.isAfter(e.getDueDate()))
                 
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
     }
@@ -1774,28 +1809,28 @@ public class 
AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
     @NotNull
     private static List<LoanRepaymentScheduleInstallment> 
getFutureInstallmentsForRefund(LoanTransaction loanTransaction,
             MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> 
installments,
-            FutureInstallmentAllocationRule futureInstallmentAllocationRule, 
Money zero) {
+            FutureInstallmentAllocationRule futureInstallmentAllocationRule) {
         List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new 
ArrayList<>();
         if 
(FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule))
 {
-            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThan(zero))
+            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThanZero())
                     .filter(e -> 
loanTransaction.isBefore(e.getDueDate())).toList();
         } else if 
(FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThan(zero))
+            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThanZero())
                     .filter(e -> loanTransaction.isBefore(e.getDueDate()))
                     
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
         } else if 
(FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThan(zero))
+            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThanZero())
                     .filter(e -> loanTransaction.isBefore(e.getDueDate()))
                     
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
         } else if 
(FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
             // try to resolve as current installment ( not due )
-            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThan(zero))
+            inAdvanceInstallments = installments.stream().filter(installment 
-> installment.getTotalPaid(currency).isGreaterThanZero())
                     .filter(e -> 
loanTransaction.isBefore(e.getDueDate())).filter(f -> 
loanTransaction.isAfter(f.getFromDate())
                             || (loanTransaction.isOn(f.getFromDate()) && 
f.getInstallmentNumber() == 1))
                     .toList();
             // if there is no current installment, resolve similar to 
LAST_INSTALLMENT
             if (inAdvanceInstallments.isEmpty()) {
-                inAdvanceInstallments = 
installments.stream().filter(installment -> 
installment.getTotalPaid(currency).isGreaterThan(zero))
+                inAdvanceInstallments = 
installments.stream().filter(installment -> 
installment.getTotalPaid(currency).isGreaterThanZero())
                         .filter(e -> loanTransaction.isBefore(e.getDueDate()))
                         
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
             }
@@ -1807,8 +1842,8 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = 
new ArrayList<>();
         Money zero = Money.zero(transactionCtx.getCurrency());
         Balances balances = new Balances(zero, zero, zero, zero);
-        transactionAmountUnprocessed = processPeriods(loanTransaction, 
transactionAmountUnprocessed, transactionCtx.getCharges(),
-                transactionMappings, balances, transactionCtx);
+        transactionAmountUnprocessed = processPeriods(loanTransaction, 
transactionAmountUnprocessed, transactionMappings, balances,
+                transactionCtx);
 
         
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), 
balances.getAggregatedInterestPortion(),
                 balances.getAggregatedFeeChargesPortion(), 
balances.getAggregatedPenaltyChargesPortion());
@@ -1817,31 +1852,28 @@ public class 
AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
         handleOverpayment(transactionAmountUnprocessed, loanTransaction, 
transactionCtx);
     }
 
-    private Money processPeriods(LoanTransaction transaction, Money 
processAmount, Set<LoanCharge> charges,
+    private Money processPeriods(LoanTransaction transaction, Money 
processAmount,
             List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances, TransactionCtx transactionCtx) {
         LoanPaymentAllocationRule allocationRule = 
getAllocationRule(transaction);
-        return processPeriods(transaction, processAmount, allocationRule, 
charges, transactionMappings, balances, transactionCtx);
+        return processPeriods(transaction, processAmount, allocationRule, 
transactionMappings, balances, transactionCtx);
     }
 
     private Money processPeriods(LoanTransaction transaction, Money 
processAmount, LoanPaymentAllocationRule allocationRule,
-            Set<LoanCharge> charges, 
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances 
balances,
-            TransactionCtx transactionCtx) {
+            List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances, TransactionCtx transactionCtx) {
         LoanScheduleProcessingType scheduleProcessingType = 
transaction.getLoan().getLoanProductRelatedDetail()
                 .getLoanScheduleProcessingType();
         if (scheduleProcessingType.isHorizontal()) {
-            return processPeriodsHorizontally(transaction, transactionCtx, 
processAmount, allocationRule, transactionMappings, charges,
-                    balances);
+            return processPeriodsHorizontally(transaction, transactionCtx, 
processAmount, allocationRule, transactionMappings, balances);
         }
         if (scheduleProcessingType.isVertical()) {
-            return processPeriodsVertically(transaction, transactionCtx, 
processAmount, allocationRule, transactionMappings, charges,
-                    balances);
+            return processPeriodsVertically(transaction, transactionCtx, 
processAmount, allocationRule, transactionMappings, balances);
         }
         return processAmount;
     }
 
     private Money processPeriodsHorizontally(LoanTransaction loanTransaction, 
TransactionCtx transactionCtx,
             Money transactionAmountUnprocessed, LoanPaymentAllocationRule 
paymentAllocationRule,
-            List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Set<LoanCharge> charges, Balances balances) {
+            List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances) {
         LinkedHashMap<DueType, List<PaymentAllocationType>> 
paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
                 
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, 
LinkedHashMap::new,
                         mapping(Function.identity(), toList())));
@@ -1849,180 +1881,205 @@ public class 
AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
         for (Map.Entry<DueType, List<PaymentAllocationType>> 
paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
             transactionAmountUnprocessed = 
processAllocationsHorizontally(loanTransaction, transactionCtx, 
transactionAmountUnprocessed,
                     paymentAllocationsEntry.getValue(), 
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings,
-                    charges, balances);
+                    balances);
         }
         return transactionAmountUnprocessed;
     }
 
-    private Money processAllocationsHorizontally(LoanTransaction 
loanTransaction, TransactionCtx transactionCtx,
-            Money transactionAmountUnprocessed, List<PaymentAllocationType> 
paymentAllocationTypes,
-            FutureInstallmentAllocationRule futureInstallmentAllocationRule,
-            List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Set<LoanCharge> charges, Balances balances) {
+    private Money processAllocationsHorizontally(LoanTransaction 
loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
+            List<PaymentAllocationType> paymentAllocationTypes, 
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
+            List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances) {
         if (MathUtil.isEmpty(transactionAmountUnprocessed)) {
             return transactionAmountUnprocessed;
         }
-
-        MonetaryCurrency currency = transactionCtx.getCurrency();
-        List<LoanRepaymentScheduleInstallment> installments = 
transactionCtx.getInstallments();
-        Money paidPortion;
-        boolean exit = false;
-        Predicate<LoanRepaymentScheduleInstallment> 
inAdvanceInstallmentsFilteringRules;
+        HorizontalPaymentAllocationContext paymentAllocationContext = new 
HorizontalPaymentAllocationContext(ctx, loanTransaction,
+                paymentAllocationTypes, futureInstallmentAllocationRule, 
transactionMappings, balances);
+        
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
         boolean interestBearingAndInterestRecalculationEnabled = 
loanTransaction.getLoan()
                 .isInterestBearingAndInterestRecalculationEnabled();
-        boolean isProgressiveCtx = transactionCtx instanceof 
ProgressiveTransactionCtx;
+        boolean isProgressiveCtx = ctx instanceof ProgressiveTransactionCtx;
 
         if (isProgressiveCtx && 
interestBearingAndInterestRecalculationEnabled) {
-            ProgressiveTransactionCtx ctx = (ProgressiveTransactionCtx) 
transactionCtx;
+            ProgressiveTransactionCtx progressiveTransactionCtx = 
(ProgressiveTransactionCtx) ctx;
             // Clear any previously skipped installments before re-evaluating
-            ctx.getSkipRepaymentScheduleInstallments().clear();
-
-            inAdvanceInstallmentsFilteringRules = installment -> 
loanTransaction.isBefore(installment.getDueDate())
-                    && (installment.isNotFullyPaidOff()
-                            || (installment.isDueBalanceZero() && 
!ctx.getSkipRepaymentScheduleInstallments().contains(installment)));
+            
progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().clear();
+            paymentAllocationContext
+                    .setInAdvanceInstallmentsFilteringRules(installment -> 
loanTransaction.isBefore(installment.getDueDate())
+                            && (installment.isNotFullyPaidOff() || 
(installment.isDueBalanceZero()
+                                    && 
!progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().contains(installment))));
         } else {
-            inAdvanceInstallmentsFilteringRules = installment -> 
loanTransaction.isBefore(installment.getDueDate())
-                    && installment.isNotFullyPaidOff();
-        }
-
-        do {
-            LoanRepaymentScheduleInstallment oldestPastDueInstallment = 
installments.stream()
-                    
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> 
loanTransaction.isAfter(e.getDueDate()))
-                    
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
-            LoanRepaymentScheduleInstallment dueInstallment = 
installments.stream()
-                    
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> 
loanTransaction.isOn(e.getDueDate()))
-                    
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
-
-            // For having similar logic we are populating installment list 
even when the future installment
-            // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence 
the list has only one element.
-            List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new 
ArrayList<>();
-            if 
(FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule))
 {
-                inAdvanceInstallments = 
installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
 //
-                        .filter(e -> loanTransaction.isBefore(e.getDueDate())) 
//
-                        .toList(); //
-            } else if 
(FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-                inAdvanceInstallments = 
installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
 //
-                        .filter(e -> loanTransaction.isBefore(e.getDueDate())) 
//
-                        
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
 //
-                        .toList(); //
-            } else if 
(FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-                inAdvanceInstallments = 
installments.stream().filter(inAdvanceInstallmentsFilteringRules)
-                        
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
 //
-                        .toList(); //
-            } else if 
(FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-                // try to resolve as current installment ( not due )
-                inAdvanceInstallments = 
installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
 //
-                        .filter(e -> loanTransaction.isBefore(e.getDueDate())) 
//
-                        .filter(f -> loanTransaction.isAfter(f.getFromDate())
-                                || (loanTransaction.isOn(f.getFromDate()) && 
f.getInstallmentNumber() == 1)) //
-                        .toList(); //
-                // if there is no current installment, resolve similar to 
LAST_INSTALLMENT
-                if (inAdvanceInstallments.isEmpty()) {
-                    inAdvanceInstallments = 
installments.stream().filter(inAdvanceInstallmentsFilteringRules)
-                            
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
 //
-                            .toList(); //
-                }
-            }
-
-            int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
-
-            for (PaymentAllocationType paymentAllocationType : 
paymentAllocationTypes) {
-                switch (paymentAllocationType.getDueType()) {
-                    case PAST_DUE -> {
-                        if (oldestPastDueInstallment != null) {
-                            Set<LoanCharge> oldestPastDueInstallmentCharges = 
getLoanChargesOfInstallment(charges, oldestPastDueInstallment,
-                                    firstNormalInstallmentNumber);
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
oldestPastDueInstallment, currency);
-                            Loan loan = loanTransaction.getLoan();
-                            if (transactionCtx instanceof 
ProgressiveTransactionCtx ctx
-                                    && 
loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) 
{
-                                paidPortion = 
handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction,
-                                        transactionAmountUnprocessed, 
balances, paymentAllocationType, oldestPastDueInstallment, ctx,
-                                        
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges);
-                            } else {
-                                paidPortion = 
processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, 
loanTransaction,
-                                        transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping,
-                                        oldestPastDueInstallmentCharges, 
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
-                            }
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(paidPortion);
-                        } else {
-                            exit = true;
+            paymentAllocationContext.setInAdvanceInstallmentsFilteringRules(
+                    installment -> 
loanTransaction.isBefore(installment.getDueDate()) && 
installment.isNotFullyPaidOff());
+        }
+        
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size()
 * 100, //
+                paymentAllocationContext, //
+                (HorizontalPaymentAllocationContext context) -> 
!context.isExitCondition()
+                        && 
context.getCtx().getInstallments().stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
+                        && 
context.getTransactionAmountUnprocessed().isGreaterThanZero(), //
+                context -> {
+                    LoanRepaymentScheduleInstallment oldestPastDueInstallment 
= context.getCtx().getInstallments().stream()
+                            
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
+                            .filter(e -> 
context.getLoanTransaction().isAfter(e.getDueDate()))
+                            
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
+                    LoanRepaymentScheduleInstallment dueInstallment = 
context.getCtx().getInstallments().stream()
+                            
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
+                            .filter(e -> 
context.getLoanTransaction().isOn(e.getDueDate()))
+                            
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
+
+                    // For having similar logic we are populating installment 
list even when the future installment
+                    // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT 
hence the list has only one element.
+                    List<LoanRepaymentScheduleInstallment> 
inAdvanceInstallments = new ArrayList<>();
+                    if 
(FutureInstallmentAllocationRule.REAMORTIZATION.equals(context.getFutureInstallmentAllocationRule()))
 {
+                        inAdvanceInstallments = 
context.getCtx().getInstallments().stream()
+                                
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
+                                .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate())) //
+                                .toList(); //
+                    } else if 
(FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule()))
 {
+                        inAdvanceInstallments = 
context.getCtx().getInstallments().stream()
+                                
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
+                                .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate())) //
+                                
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
 //
+                                .toList(); //
+                    } else if 
(FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule()))
 {
+                        inAdvanceInstallments = 
context.getCtx().getInstallments().stream()
+                                
.filter(context.getInAdvanceInstallmentsFilteringRules())
+                                
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
 //
+                                .toList(); //
+                    } else if 
(FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule()))
 {
+                        // try to resolve as current installment ( not due )
+                        inAdvanceInstallments = 
context.getCtx().getInstallments().stream()
+                                
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
+                                .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate())) //
+                                .filter(f -> 
context.getLoanTransaction().isAfter(f.getFromDate())
+                                        || 
(context.getLoanTransaction().isOn(f.getFromDate()) && f.getInstallmentNumber() 
== 1)) //
+                                .toList(); //
+                        // if there is no current installment, resolve similar 
to LAST_INSTALLMENT
+                        if (inAdvanceInstallments.isEmpty()) {
+                            inAdvanceInstallments = 
context.getCtx().getInstallments().stream()
+                                    
.filter(context.getInAdvanceInstallmentsFilteringRules())
+                                    
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
 //
+                                    .toList(); //
                         }
                     }
-                    case DUE -> {
-                        if (dueInstallment != null) {
-                            Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(charges, dueInstallment,
-                                    firstNormalInstallmentNumber);
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
dueInstallment, currency);
-                            Loan loan = loanTransaction.getLoan();
-                            if (transactionCtx instanceof 
ProgressiveTransactionCtx ctx
-                                    && 
loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) 
{
-                                paidPortion = 
handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction,
-                                        transactionAmountUnprocessed, 
balances, paymentAllocationType, dueInstallment, ctx,
-                                        
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges);
-                            } else {
-                                paidPortion = 
processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
-                                        transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
-                                        balances, 
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
-                            }
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(paidPortion);
-                        } else {
-                            exit = true;
-                        }
-                    }
-                    case IN_ADVANCE -> {
-                        int numberOfInstallments = 
inAdvanceInstallments.size();
-                        if (numberOfInstallments > 0) {
-                            // This will be the same amount as 
transactionAmountUnprocessed in case of the future
-                            // installment allocation is NEXT_INSTALLMENT or 
LAST_INSTALLMENT
-                            Money evenPortion = 
transactionAmountUnprocessed.dividedBy(numberOfInstallments, 
MoneyHelper.getMathContext());
-                            // Adjustment might be needed due to the divide 
operation and the rounding mode
-                            Money balanceAdjustment = 
transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
-                            if 
(evenPortion.add(balanceAdjustment).isLessThanZero()) {
-                                // Note: Rounding mode DOWN grants that 
evenPortion cant pay more than unprocessed
-                                // transaction amount.
-                                evenPortion = 
transactionAmountUnprocessed.dividedBy(numberOfInstallments,
-                                        new 
MathContext(MoneyHelper.getMathContext().getPrecision(), RoundingMode.DOWN));
-                                balanceAdjustment = 
transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
-                            }
-
-                            for (LoanRepaymentScheduleInstallment 
inAdvanceInstallment : inAdvanceInstallments) {
-                                Set<LoanCharge> inAdvanceInstallmentCharges = 
getLoanChargesOfInstallment(charges, inAdvanceInstallment,
-                                        firstNormalInstallmentNumber);
 
-                                LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                        transactionMappings, loanTransaction, 
inAdvanceInstallment, currency);
-
-                                Loan loan = loanTransaction.getLoan();
-                                // Adjust the portion for the last installment
-                                if 
(inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 
1))) {
-                                    evenPortion = 
evenPortion.add(balanceAdjustment);
+                    int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper
+                            
.fetchFirstNormalInstallmentNumber(context.getCtx().getInstallments());
+
+                    for (PaymentAllocationType paymentAllocationType : 
context.getPaymentAllocationTypes()) {
+                        switch (paymentAllocationType.getDueType()) {
+                            case PAST_DUE -> {
+                                if (oldestPastDueInstallment != null) {
+                                    Set<LoanCharge> 
oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
+                                            context.getCtx().getCharges(), 
oldestPastDueInstallment, firstNormalInstallmentNumber);
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), oldestPastDueInstallment,
+                                            context.getCtx().getCurrency());
+                                    Loan loan = 
context.getLoanTransaction().getLoan();
+                                    if (context.getCtx() instanceof 
ProgressiveTransactionCtx progressiveTransactionCtx
+                                            && 
loan.isInterestBearingAndInterestRecalculationEnabled()
+                                            && 
!progressiveTransactionCtx.isChargedOff()) {
+                                        context.setAllocatedAmount(
+                                                
handlingPaymentAllocationForInterestBearingProgressiveLoan(context.getLoanTransaction(),
+                                                        
context.getTransactionAmountUnprocessed(), context.getBalances(),
+                                                        paymentAllocationType, 
oldestPastDueInstallment, progressiveTransactionCtx,
+                                                        
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges));
+                                    } else {
+                                        
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, 
oldestPastDueInstallment,
+                                                context.getLoanTransaction(), 
context.getTransactionAmountUnprocessed(),
+                                                
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
+                                                context.getBalances(), 
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+                                    }
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                } else {
+                                    context.setExitCondition(true);
                                 }
-                                if (transactionCtx instanceof 
ProgressiveTransactionCtx ctx
-                                        && 
loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) 
{
-                                    paidPortion = 
handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, 
evenPortion,
-                                            balances, paymentAllocationType, 
inAdvanceInstallment, ctx,
-                                            
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges);
+                            }
+                            case DUE -> {
+                                if (dueInstallment != null) {
+                                    Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(context.getCtx().getCharges(),
+                                            dueInstallment, 
firstNormalInstallmentNumber);
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), dueInstallment,
+                                            context.getCtx().getCurrency());
+                                    Loan loan = 
context.getLoanTransaction().getLoan();
+                                    if (context.getCtx() instanceof 
ProgressiveTransactionCtx progressiveTransactionCtx
+                                            && 
loan.isInterestBearingAndInterestRecalculationEnabled()
+                                            && 
!progressiveTransactionCtx.isChargedOff()) {
+                                        
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
+                                                context.getLoanTransaction(), 
context.getTransactionAmountUnprocessed(),
+                                                context.getBalances(), 
paymentAllocationType, dueInstallment, progressiveTransactionCtx,
+                                                
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges));
+                                    } else {
+                                        
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, 
dueInstallment,
+                                                context.getLoanTransaction(), 
context.getTransactionAmountUnprocessed(),
+                                                
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, 
context.getBalances(),
+                                                
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+                                    }
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                } else {
+                                    context.setExitCondition(true);
+                                }
+                            }
+                            case IN_ADVANCE -> {
+                                int numberOfInstallments = 
inAdvanceInstallments.size();
+                                if (numberOfInstallments > 0) {
+                                    // This will be the same amount as 
transactionAmountUnprocessed in case of the
+                                    // future
+                                    // installment allocation is 
NEXT_INSTALLMENT or LAST_INSTALLMENT
+                                    Money evenPortion = 
context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
+                                            MoneyHelper.getMathContext());
+                                    // Adjustment might be needed due to the 
divide operation and the rounding mode
+                                    Money balanceAdjustment = 
context.getTransactionAmountUnprocessed()
+                                            
.minus(evenPortion.multipliedBy(numberOfInstallments));
+                                    if 
(evenPortion.add(balanceAdjustment).isLessThanZero()) {
+                                        // Note: Rounding mode DOWN grants 
that evenPortion cant pay more than
+                                        // unprocessed
+                                        // transaction amount.
+                                        evenPortion = 
context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
+                                                new 
MathContext(MoneyHelper.getMathContext().getPrecision(), RoundingMode.DOWN));
+                                        balanceAdjustment = 
context.getTransactionAmountUnprocessed()
+                                                
.minus(evenPortion.multipliedBy(numberOfInstallments));
+                                    }
+
+                                    for (LoanRepaymentScheduleInstallment 
inAdvanceInstallment : inAdvanceInstallments) {
+                                        Set<LoanCharge> 
inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
+                                                context.getCtx().getCharges(), 
inAdvanceInstallment, firstNormalInstallmentNumber);
+
+                                        
LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                                
context.getTransactionMappings(), context.getLoanTransaction(), 
inAdvanceInstallment,
+                                                
context.getCtx().getCurrency());
+
+                                        Loan loan = 
context.getLoanTransaction().getLoan();
+                                        // Adjust the portion for the last 
installment
+                                        if 
(inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 
1))) {
+                                            evenPortion = 
evenPortion.add(balanceAdjustment);
+                                        }
+                                        if (context.getCtx() instanceof 
ProgressiveTransactionCtx progressiveTransactionCtx
+                                                && 
loan.isInterestBearingAndInterestRecalculationEnabled()
+                                                && 
!progressiveTransactionCtx.isChargedOff()) {
+                                            
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
+                                                    
context.getLoanTransaction(), evenPortion, context.getBalances(), 
paymentAllocationType,
+                                                    inAdvanceInstallment, 
progressiveTransactionCtx,
+                                                    
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges));
+                                        } else {
+                                            
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, 
inAdvanceInstallment,
+                                                    
context.getLoanTransaction(), evenPortion, 
loanTransactionToRepaymentScheduleMapping,
+                                                    
inAdvanceInstallmentCharges, context.getBalances(),
+                                                    
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+                                        }
+                                        
context.setTransactionAmountUnprocessed(
+                                                
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                    }
                                 } else {
-                                    paidPortion = 
processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, 
loanTransaction,
-                                            evenPortion, 
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, 
balances,
-                                            
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
+                                    context.setExitCondition(true);
                                 }
-                                transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(paidPortion);
                             }
-                        } else {
-                            exit = true;
                         }
                     }
-                }
-            }
-        }
-        // We are allocating till there is no pending installment or there is 
no more unprocessed transaction amount
-        // or there is no more outstanding balance of the allocation type
-        while (!exit && 
installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
-                && transactionAmountUnprocessed.isGreaterThanZero());
-        return transactionAmountUnprocessed;
+                });
+        return paymentAllocationContext.getTransactionAmountUnprocessed();
     }
 
     private Money 
handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTransaction 
loanTransaction,
@@ -2115,115 +2172,146 @@ public class 
AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
 
     private Money processPeriodsVertically(LoanTransaction loanTransaction, 
TransactionCtx ctx, Money transactionAmountUnprocessed,
             LoanPaymentAllocationRule paymentAllocationRule, 
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
-            Set<LoanCharge> charges, Balances balances) {
-        MonetaryCurrency currency = ctx.getCurrency();
-        List<LoanRepaymentScheduleInstallment> installments = 
ctx.getInstallments();
-        int firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
+            Balances balances) {
+        VerticalPaymentAllocationContext paymentAllocationContext = new 
VerticalPaymentAllocationContext(ctx, loanTransaction,
+                paymentAllocationRule.getFutureInstallmentAllocationRule(), 
transactionMappings, balances);
+        
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
         for (PaymentAllocationType paymentAllocationType : 
paymentAllocationRule.getAllocationTypes()) {
-            FutureInstallmentAllocationRule futureInstallmentAllocationRule = 
paymentAllocationRule.getFutureInstallmentAllocationRule();
-            LoanRepaymentScheduleInstallment currentInstallment = null;
-            Money paidPortion = Money.zero(currency);
-            do {
-                Predicate<LoanRepaymentScheduleInstallment> predicate = 
getFilterPredicate(paymentAllocationType, currency);
-                switch (paymentAllocationType.getDueType()) {
-                    case PAST_DUE -> {
-                        currentInstallment = 
installments.stream().filter(predicate).filter(e -> 
loanTransaction.isAfter(e.getDueDate()))
-                                
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
-                        if (currentInstallment != null) {
-                            Set<LoanCharge> oldestPastDueInstallmentCharges = 
getLoanChargesOfInstallment(charges, currentInstallment,
-                                    firstNormalInstallmentNumber);
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
currentInstallment, currency);
-                            paidPortion = 
processPaymentAllocation(paymentAllocationType, currentInstallment, 
loanTransaction,
-                                    transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping,
-                                    oldestPastDueInstallmentCharges, balances, 
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(paidPortion);
-                        }
-                    }
-                    case DUE -> {
-                        currentInstallment = 
installments.stream().filter(predicate).filter(e -> 
loanTransaction.isOn(e.getDueDate()))
-                                
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
-                        if (currentInstallment != null) {
-                            Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(charges, currentInstallment,
-                                    firstNormalInstallmentNumber);
-                            LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                    transactionMappings, loanTransaction, 
currentInstallment, currency);
-                            paidPortion = 
processPaymentAllocation(paymentAllocationType, currentInstallment, 
loanTransaction,
-                                    transactionAmountUnprocessed, 
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
-                                    balances, 
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
-                            transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(paidPortion);
-                        }
-                    }
-                    case IN_ADVANCE -> {
-                        // For having similar logic we are populating 
installment list even when the future installment
-                        // allocation rule is NEXT_INSTALLMENT or 
LAST_INSTALLMENT hence the list has only one element.
-                        List<LoanRepaymentScheduleInstallment> 
currentInstallments = new ArrayList<>();
-                        if 
(FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule))
 {
-                            currentInstallments = 
installments.stream().filter(predicate)
-                                    .filter(e -> 
loanTransaction.isBefore(e.getDueDate())).toList();
-                        } else if 
(FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-                            currentInstallments = 
installments.stream().filter(predicate)
-                                    .filter(e -> 
loanTransaction.isBefore(e.getDueDate()))
-                                    
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
-                        } else if 
(FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-                            currentInstallments = 
installments.stream().filter(predicate)
-                                    .filter(e -> 
loanTransaction.isBefore(e.getDueDate()))
-                                    
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
-                        } else if 
(FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
 {
-                            // get current installment where from date < 
transaction date < to date OR transaction date
-                            // is on first installment's first day ( from day )
-                            currentInstallments = 
installments.stream().filter(predicate)
-                                    .filter(e -> 
loanTransaction.isBefore(e.getDueDate()))
-                                    .filter(f -> 
loanTransaction.isAfter(f.getFromDate())
-                                            || 
(loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
-                                    .toList();
-                            // if there is no current in advance installment 
resolve similar to LAST_INSTALLMENT
-                            if (currentInstallments.isEmpty()) {
-                                currentInstallments = 
installments.stream().filter(predicate)
-                                        .filter(e -> 
loanTransaction.isBefore(e.getDueDate()))
-                                        
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
-                                        .toList();
+            
paymentAllocationContext.setAllocatedAmount(Money.zero(ctx.getCurrency()));
+            paymentAllocationContext.setInstallment(null);
+            
paymentAllocationContext.setPaymentAllocationType(paymentAllocationType);
+            
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size()
 * 100, //
+                    paymentAllocationContext, //
+                    (VerticalPaymentAllocationContext context) -> 
context.getInstallment() != null
+                            && 
context.getTransactionAmountUnprocessed().isGreaterThanZero()
+                            && 
context.getAllocatedAmount().isGreaterThanZero(), //
+                    context -> {
+                        Predicate<LoanRepaymentScheduleInstallment> predicate 
= getFilterPredicate(context.getPaymentAllocationType(),
+                                context.getCtx().getCurrency());
+                        switch 
(context.getPaymentAllocationType().getDueType()) {
+                            case PAST_DUE -> {
+                                
context.setInstallment(context.getCtx().getInstallments().stream().filter(predicate)
+                                        .filter(e -> 
context.getLoanTransaction().isAfter(e.getDueDate()))
+                                        
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null));
+                                if (context.getInstallment() != null) {
+                                    Set<LoanCharge> 
oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
+                                            context.getCtx().getCharges(), 
context.getInstallment(),
+                                            
context.getFirstNormalInstallmentNumber());
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), context.getInstallment(),
+                                            context.getCtx().getCurrency());
+                                    context.setAllocatedAmount(
+                                            
processPaymentAllocation(context.getPaymentAllocationType(), 
context.getInstallment(),
+                                                    
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+                                                    
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
+                                                    context.getBalances(), 
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
+                                }
                             }
-                        }
-                        int numberOfInstallments = currentInstallments.size();
-                        paidPortion = Money.zero(currency);
-                        if (numberOfInstallments > 0) {
-                            // This will be the same amount as 
transactionAmountUnprocessed in case of the future
-                            // installment allocation is NEXT_INSTALLMENT or 
LAST_INSTALLMENT
-                            Money evenPortion = 
transactionAmountUnprocessed.dividedBy(numberOfInstallments, 
MoneyHelper.getMathContext());
-                            // Adjustment might be needed due to the divide 
operation and the rounding mode
-                            Money balanceAdjustment = 
transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
-                            for (LoanRepaymentScheduleInstallment 
internalCurrentInstallment : currentInstallments) {
-                                currentInstallment = 
internalCurrentInstallment;
-                                Set<LoanCharge> inAdvanceInstallmentCharges = 
getLoanChargesOfInstallment(charges, currentInstallment,
-                                        firstNormalInstallmentNumber);
-                                // Adjust the portion for the last installment
-                                if 
(internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments 
- 1))) {
-                                    evenPortion = 
evenPortion.add(balanceAdjustment);
+                            case DUE -> {
+                                
context.setInstallment(context.getCtx().getInstallments().stream().filter(predicate)
+                                        .filter(e -> 
context.getLoanTransaction().isOn(e.getDueDate()))
+                                        
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null));
+                                if (context.getInstallment() != null) {
+                                    Set<LoanCharge> dueInstallmentCharges = 
getLoanChargesOfInstallment(context.getCtx().getCharges(),
+                                            context.getInstallment(), 
context.getFirstNormalInstallmentNumber());
+                                    LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                            context.getTransactionMappings(), 
context.getLoanTransaction(), context.getInstallment(),
+                                            context.getCtx().getCurrency());
+                                    context.setAllocatedAmount(
+                                            
processPaymentAllocation(context.getPaymentAllocationType(), 
context.getInstallment(),
+                                                    
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
+                                                    
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, 
context.getBalances(),
+                                                    
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
+                                    context.setTransactionAmountUnprocessed(
+                                            
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
                                 }
-                                LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
-                                        transactionMappings, loanTransaction, 
currentInstallment, currency);
-                                Money internalPaidPortion = 
processPaymentAllocation(paymentAllocationType, currentInstallment,
-                                        loanTransaction, evenPortion, 
loanTransactionToRepaymentScheduleMapping,
-                                        inAdvanceInstallmentCharges, balances, 
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
-                                // Some extra logic to allocate as much as 
possible across the installments if the
-                                // outstanding balances are different
-                                if (internalPaidPortion.isGreaterThanZero()) {
-                                    paidPortion = internalPaidPortion;
+                            }
+                            case IN_ADVANCE -> {
+                                // For having similar logic we are populating 
installment list even when the future
+                                // installment
+                                // allocation rule is NEXT_INSTALLMENT or 
LAST_INSTALLMENT hence the list has only one
+                                // element.
+                                List<LoanRepaymentScheduleInstallment> 
currentInstallments = new ArrayList<>();
+                                if 
(FutureInstallmentAllocationRule.REAMORTIZATION.equals(context.getFutureInstallmentAllocationRule()))
 {
+                                    currentInstallments = 
context.getCtx().getInstallments().stream().filter(predicate)
+                                            .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate())).toList();
+                                } else if 
(FutureInstallmentAllocationRule.NEXT_INSTALLMENT
+                                        
.equals(context.getFutureInstallmentAllocationRule())) {
+                                    currentInstallments = 
context.getCtx().getInstallments().stream().filter(predicate)
+                                            .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate()))
+                                            
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
+                                            .toList();
+                                } else if 
(FutureInstallmentAllocationRule.LAST_INSTALLMENT
+                                        
.equals(context.getFutureInstallmentAllocationRule())) {
+                                    currentInstallments = 
context.getCtx().getInstallments().stream().filter(predicate)
+                                            .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate()))
+                                            
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
+                                            .toList();
+                                } else if 
(FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT
+                                        
.equals(context.getFutureInstallmentAllocationRule())) {
+                                    // get current installment where from date 
< transaction date < to date OR
+                                    // transaction date
+                                    // is on first installment's first day ( 
from day )
+                                    currentInstallments = 
context.getCtx().getInstallments().stream().filter(predicate)
+                                            .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate()))
+                                            .filter(f -> 
context.getLoanTransaction().isAfter(f.getFromDate())
+                                                    || 
(context.getLoanTransaction().isOn(f.getFromDate())
+                                                            && 
f.getInstallmentNumber() == 1))
+                                            .toList();
+                                    // if there is no current in advance 
installment resolve similar to LAST_INSTALLMENT
+                                    if (currentInstallments.isEmpty()) {
+                                        currentInstallments = 
context.getCtx().getInstallments().stream().filter(predicate)
+                                                .filter(e -> 
context.getLoanTransaction().isBefore(e.getDueDate()))
+                                                
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
+                                                .toList();
+                                    }
+                                }
+                                int numberOfInstallments = 
currentInstallments.size();
+                                
context.setAllocatedAmount(Money.zero(context.getCtx().getCurrency()));
+                                if (numberOfInstallments > 0) {
+                                    // This will be the same amount as 
transactionAmountUnprocessed in case of the
+                                    // future
+                                    // installment allocation is 
NEXT_INSTALLMENT or LAST_INSTALLMENT
+                                    Money evenPortion = 
context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
+                                            MoneyHelper.getMathContext());
+                                    // Adjustment might be needed due to the 
divide operation and the rounding mode
+                                    Money balanceAdjustment = 
context.getTransactionAmountUnprocessed()
+                                            
.minus(evenPortion.multipliedBy(numberOfInstallments));
+                                    for (LoanRepaymentScheduleInstallment 
internalCurrentInstallment : currentInstallments) {
+                                        
context.setInstallment(internalCurrentInstallment);
+                                        Set<LoanCharge> 
inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
+                                                context.getCtx().getCharges(), 
context.getInstallment(),
+                                                
context.getFirstNormalInstallmentNumber());
+                                        // Adjust the portion for the last 
installment
+                                        if 
(internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments 
- 1))) {
+                                            evenPortion = 
evenPortion.add(balanceAdjustment);
+                                        }
+                                        
LoanTransactionToRepaymentScheduleMapping 
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+                                                
context.getTransactionMappings(), context.getLoanTransaction(), 
context.getInstallment(),
+                                                
context.getCtx().getCurrency());
+                                        Money internalPaidPortion = 
processPaymentAllocation(context.getPaymentAllocationType(),
+                                                context.getInstallment(), 
context.getLoanTransaction(), evenPortion,
+                                                
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
+                                                context.getBalances(), 
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
+                                        // Some extra logic to allocate as 
much as possible across the installments if
+                                        // the
+                                        // outstanding balances are different
+                                        if 
(internalPaidPortion.isGreaterThanZero()) {
+                                            
context.setAllocatedAmount(internalPaidPortion);
+                                        }
+                                        
context.setTransactionAmountUnprocessed(
+                                                
context.getTransactionAmountUnprocessed().minus(internalPaidPortion));
+                                    }
+                                } else {
+                                    context.setInstallment(null);
                                 }
-                                transactionAmountUnprocessed = 
transactionAmountUnprocessed.minus(internalPaidPortion);
                             }
-                        } else {
-                            currentInstallment = null;
                         }
-                    }
-                }
-            }
-            // We are allocating till there is no pending installment or there 
is no more unprocessed transaction amount
-            // or there is no more outstanding balance of the allocation type
-            while (currentInstallment != null && 
transactionAmountUnprocessed.isGreaterThanZero() && 
paidPortion.isGreaterThanZero());
+                    });
         }
-        return transactionAmountUnprocessed;
+        return paymentAllocationContext.getTransactionAmountUnprocessed();
     }
 
     private Predicate<LoanRepaymentScheduleInstallment> 
getFilterPredicate(PaymentAllocationType paymentAllocationType,
@@ -2454,4 +2542,63 @@ public class AdvancedPaymentScheduleTransactionProcessor 
extends AbstractLoanRep
         
ctx.getChangedTransactionDetail().addNewTransactionChangeBeforeExistingOne(new 
TransactionChangeData(null, newAccrualTransaction),
                 chargeOffTransaction);
     }
+
+    @Getter
+    @Setter
+    private static class VerticalPaymentAllocationContext implements 
LoopContext {
+
+        private final TransactionCtx ctx;
+        private final LoanTransaction loanTransaction;
+        private final FutureInstallmentAllocationRule 
futureInstallmentAllocationRule;
+        private final List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings;
+        private final Balances balances;
+        private final int firstNormalInstallmentNumber;
+        private LoanRepaymentScheduleInstallment installment;
+        private Money transactionAmountUnprocessed;
+        private Money allocatedAmount;
+        private PaymentAllocationType paymentAllocationType;
+
+        VerticalPaymentAllocationContext(TransactionCtx ctx, LoanTransaction 
loanTransaction,
+                FutureInstallmentAllocationRule 
futureInstallmentAllocationRule,
+                List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances) {
+            this.ctx = ctx;
+            this.loanTransaction = loanTransaction;
+            this.futureInstallmentAllocationRule = 
futureInstallmentAllocationRule;
+            this.transactionMappings = transactionMappings;
+            this.balances = balances;
+            firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper
+                    
.fetchFirstNormalInstallmentNumber(getCtx().getInstallments());
+        }
+    }
+
+    @Getter
+    @Setter
+    private static class HorizontalPaymentAllocationContext implements 
LoopContext {
+
+        private final TransactionCtx ctx;
+        private final LoanTransaction loanTransaction;
+        private final List<PaymentAllocationType> paymentAllocationTypes;
+        private final FutureInstallmentAllocationRule 
futureInstallmentAllocationRule;
+        private final List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings;
+        private final Balances balances;
+        private final int firstNormalInstallmentNumber;
+        private LoanRepaymentScheduleInstallment installment;
+        private Money transactionAmountUnprocessed;
+        private Money allocatedAmount;
+        private boolean exitCondition;
+        private Predicate<LoanRepaymentScheduleInstallment> 
inAdvanceInstallmentsFilteringRules;
+
+        HorizontalPaymentAllocationContext(TransactionCtx ctx, LoanTransaction 
loanTransaction,
+                List<PaymentAllocationType> paymentAllocationTypes, 
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
+                List<LoanTransactionToRepaymentScheduleMapping> 
transactionMappings, Balances balances) {
+            this.ctx = ctx;
+            this.loanTransaction = loanTransaction;
+            this.paymentAllocationTypes = paymentAllocationTypes;
+            this.futureInstallmentAllocationRule = 
futureInstallmentAllocationRule;
+            this.transactionMappings = transactionMappings;
+            this.balances = balances;
+            firstNormalInstallmentNumber = 
LoanRepaymentScheduleProcessingWrapper
+                    
.fetchFirstNormalInstallmentNumber(getCtx().getInstallments());
+        }
+    }
 }

Reply via email to