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());
+ }
+ }
}