FINERACT-2181: Introduce Loop guard to avoid endless loops
This commit is contained in:
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
+425
-278
@@ -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);
|
||||
HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction,
|
||||
paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances);
|
||||
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = getFutureInstallmentsForRefund(loanTransaction, currency,
|
||||
installments, futureInstallmentAllocationRule, zero);
|
||||
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());
|
||||
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
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(charges, latestPastDueInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
|
||||
context.getCtx().getCharges(), 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);
|
||||
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 {
|
||||
break outerLoop;
|
||||
context.setExitCondition(true);
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
if (dueInstallment != null) {
|
||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||
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);
|
||||
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 {
|
||||
break outerLoop;
|
||||
context.setExitCondition(true);
|
||||
}
|
||||
}
|
||||
case IN_ADVANCE -> {
|
||||
int numberOfInstallments = inAdvanceInstallments.size();
|
||||
if (numberOfInstallments > 0) {
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||
MoneyHelper.getMathContext());
|
||||
Money balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
|
||||
context.getCtx().getCharges(), inAdvanceInstallment, firstNormalInstallmentNumber);
|
||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
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 {
|
||||
break outerLoop;
|
||||
context.setExitCondition(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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()) {
|
||||
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 -> {
|
||||
currentInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero);
|
||||
if (currentInstallment != null) {
|
||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
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(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
|
||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
currentInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero);
|
||||
if (currentInstallment != null) {
|
||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
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(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
||||
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(loanTransaction, currency,
|
||||
installments, futureInstallmentAllocationRule, zero);
|
||||
List<LoanRepaymentScheduleInstallment> currentInstallments = getFutureInstallmentsForRefund(
|
||||
context.getLoanTransaction(), context.getCtx().getCurrency(), context.getCtx().getInstallments(),
|
||||
context.getFutureInstallmentAllocationRule());
|
||||
int numberOfInstallments = currentInstallments.size();
|
||||
refundedPortion = zero;
|
||||
context.setAllocatedAmount(Money.zero(context.getCtx().getCurrency()));
|
||||
if (numberOfInstallments > 0) {
|
||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||
MoneyHelper.getMathContext());
|
||||
Money balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||
currentInstallment = internalCurrentInstallment;
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
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(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
|
||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||
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()) {
|
||||
refundedPortion = internalUnpaidPortion;
|
||||
context.setAllocatedAmount(internalUnpaidPortion);
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalUnpaidPortion);
|
||||
context.setTransactionAmountUnprocessed(
|
||||
context.getTransactionAmountUnprocessed().minus(internalUnpaidPortion));
|
||||
}
|
||||
} else {
|
||||
currentInstallment = null;
|
||||
context.setInstallment(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();
|
||||
paymentAllocationContext.setInAdvanceInstallmentsFilteringRules(
|
||||
installment -> loanTransaction.isBefore(installment.getDueDate()) && installment.isNotFullyPaidOff());
|
||||
}
|
||||
|
||||
do {
|
||||
LoanRepaymentScheduleInstallment oldestPastDueInstallment = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||
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 = installments.stream()
|
||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
||||
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(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
|
||||
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(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
|
||||
} 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(futureInstallmentAllocationRule)) {
|
||||
inAdvanceInstallments = installments.stream().filter(inAdvanceInstallmentsFilteringRules)
|
||||
} 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(futureInstallmentAllocationRule)) {
|
||||
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule())) {
|
||||
// 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)) //
|
||||
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 = installments.stream().filter(inAdvanceInstallmentsFilteringRules)
|
||||
inAdvanceInstallments = context.getCtx().getInstallments().stream()
|
||||
.filter(context.getInAdvanceInstallmentsFilteringRules())
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
||||
.toList(); //
|
||||
}
|
||||
}
|
||||
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
|
||||
.fetchFirstNormalInstallmentNumber(context.getCtx().getInstallments());
|
||||
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
for (PaymentAllocationType paymentAllocationType : context.getPaymentAllocationTypes()) {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
case PAST_DUE -> {
|
||||
if (oldestPastDueInstallment != null) {
|
||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, oldestPastDueInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
|
||||
context.getCtx().getCharges(), 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);
|
||||
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 {
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
|
||||
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment,
|
||||
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
|
||||
context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
context.setTransactionAmountUnprocessed(
|
||||
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||
} else {
|
||||
exit = true;
|
||||
context.setExitCondition(true);
|
||||
}
|
||||
}
|
||||
case DUE -> {
|
||||
if (dueInstallment != null) {
|
||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||
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);
|
||||
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 {
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
|
||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, dueInstallment,
|
||||
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
context.setTransactionAmountUnprocessed(
|
||||
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||
} else {
|
||||
exit = true;
|
||||
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
|
||||
// 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());
|
||||
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||
MoneyHelper.getMathContext());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
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
|
||||
// Note: Rounding mode DOWN grants that evenPortion cant pay more than
|
||||
// unprocessed
|
||||
// transaction amount.
|
||||
evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments,
|
||||
evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||
new MathContext(MoneyHelper.getMathContext().getPrecision(), RoundingMode.DOWN));
|
||||
balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
}
|
||||
|
||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
|
||||
context.getCtx().getCharges(), inAdvanceInstallment, firstNormalInstallmentNumber);
|
||||
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
|
||||
context.getTransactionMappings(), context.getLoanTransaction(), inAdvanceInstallment,
|
||||
context.getCtx().getCurrency());
|
||||
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
Loan loan = context.getLoanTransaction().getLoan();
|
||||
// Adjust the portion for the last installment
|
||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||
evenPortion = evenPortion.add(balanceAdjustment);
|
||||
}
|
||||
if (transactionCtx instanceof ProgressiveTransactionCtx ctx
|
||||
&& loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) {
|
||||
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, evenPortion,
|
||||
balances, paymentAllocationType, inAdvanceInstallment, ctx,
|
||||
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges);
|
||||
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||
&& loan.isInterestBearingAndInterestRecalculationEnabled()
|
||||
&& !progressiveTransactionCtx.isChargedOff()) {
|
||||
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
|
||||
context.getLoanTransaction(), evenPortion, context.getBalances(), paymentAllocationType,
|
||||
inAdvanceInstallment, progressiveTransactionCtx,
|
||||
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges));
|
||||
} else {
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
||||
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, inAdvanceInstallment,
|
||||
context.getLoanTransaction(), evenPortion, loanTransactionToRepaymentScheduleMapping,
|
||||
inAdvanceInstallmentCharges, context.getBalances(),
|
||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
context.setTransactionAmountUnprocessed(
|
||||
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||
}
|
||||
} else {
|
||||
exit = true;
|
||||
context.setExitCondition(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()) {
|
||||
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 -> {
|
||||
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);
|
||||
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(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
|
||||
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
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);
|
||||
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(
|
||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
|
||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
||||
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()));
|
||||
}
|
||||
}
|
||||
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.
|
||||
// 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
|
||||
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 = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
.filter(f -> loanTransaction.isAfter(f.getFromDate())
|
||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
||||
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 = installments.stream().filter(predicate)
|
||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||
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();
|
||||
paidPortion = Money.zero(currency);
|
||||
context.setAllocatedAmount(Money.zero(context.getCtx().getCurrency()));
|
||||
if (numberOfInstallments > 0) {
|
||||
// This will be the same amount as transactionAmountUnprocessed in case of the future
|
||||
// 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());
|
||||
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||
MoneyHelper.getMathContext());
|
||||
// Adjustment might be needed due to the divide operation and the rounding mode
|
||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
Money balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||
currentInstallment = internalCurrentInstallment;
|
||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
||||
firstNormalInstallmentNumber);
|
||||
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(
|
||||
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
|
||||
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()) {
|
||||
paidPortion = internalPaidPortion;
|
||||
context.setAllocatedAmount(internalPaidPortion);
|
||||
}
|
||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalPaidPortion);
|
||||
context.setTransactionAmountUnprocessed(
|
||||
context.getTransactionAmountUnprocessed().minus(internalPaidPortion));
|
||||
}
|
||||
} else {
|
||||
currentInstallment = null;
|
||||
context.setInstallment(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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user