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.LoanPreCloseInterestCalculationStrategy;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
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.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -858,7 +860,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
List<LoanTransaction> remainingTransactions = new ArrayList<>(overpaidTransactions);
|
List<LoanTransaction> remainingTransactions = new ArrayList<>(overpaidTransactions);
|
||||||
MonetaryCurrency currency = ctx.getCurrency();
|
MonetaryCurrency currency = ctx.getCurrency();
|
||||||
MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder();
|
MoneyHolder overpaymentHolder = ctx.getOverpaymentHolder();
|
||||||
Set<LoanCharge> charges = ctx.getCharges();
|
|
||||||
Money zero = Money.zero(currency);
|
Money zero = Money.zero(currency);
|
||||||
for (LoanTransaction transaction : overpaidTransactions) {
|
for (LoanTransaction transaction : overpaidTransactions) {
|
||||||
Money overpayment = transaction.getOverPaymentPortion(currency);
|
Money overpayment = transaction.getOverPaymentPortion(currency);
|
||||||
@@ -880,7 +881,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
Balances balances = new Balances(zero, zero, zero, zero);
|
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));
|
processTransaction.setOverPayments(MathUtil.plus(overpayment, unprocessed));
|
||||||
overpaymentHolder.setMoneyObject(MathUtil.plus(ctxOverpayment, unprocessed));
|
overpaymentHolder.setMoneyObject(MathUtil.plus(ctxOverpayment, unprocessed));
|
||||||
@@ -1139,8 +1140,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
Money zero = Money.zero(currency);
|
Money zero = Money.zero(currency);
|
||||||
Balances balances = new Balances(zero, zero, zero, zero);
|
Balances balances = new Balances(zero, zero, zero, zero);
|
||||||
LoanPaymentAllocationRule defaultAllocationRule = getDefaultAllocationRule(loanTransaction.getLoan());
|
LoanPaymentAllocationRule defaultAllocationRule = getDefaultAllocationRule(loanTransaction.getLoan());
|
||||||
Money transactionAmountUnprocessed = processPeriods(loanTransaction, overpayment, defaultAllocationRule, Set.of(),
|
Money transactionAmountUnprocessed = processPeriods(loanTransaction, overpayment, defaultAllocationRule, transactionMappings,
|
||||||
transactionMappings, balances, transactionCtx);
|
balances, transactionCtx);
|
||||||
|
|
||||||
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
|
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
|
||||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||||
@@ -1609,164 +1610,198 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
||||||
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
|
||||||
MonetaryCurrency currency = ctx.getCurrency();
|
HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction,
|
||||||
Money zero = Money.zero(currency);
|
paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances);
|
||||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
|
||||||
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,
|
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size() * 100, //
|
||||||
installments, futureInstallmentAllocationRule, zero);
|
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);
|
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = getFutureInstallmentsForRefund(
|
||||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
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()) {
|
switch (paymentAllocationType.getDueType()) {
|
||||||
case PAST_DUE -> {
|
case PAST_DUE -> {
|
||||||
if (latestPastDueInstallment != null) {
|
if (latestPastDueInstallment != null) {
|
||||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, latestPastDueInstallment,
|
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
|
||||||
firstNormalInstallmentNumber);
|
context.getCtx().getCharges(), latestPastDueInstallment, firstNormalInstallmentNumber);
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, latestPastDueInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), latestPastDueInstallment,
|
||||||
refundedPortion = processPaymentAllocation(paymentAllocationType, latestPastDueInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
|
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, latestPastDueInstallment,
|
||||||
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
|
||||||
|
context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
} else {
|
} else {
|
||||||
break outerLoop;
|
context.setExitCondition(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case DUE -> {
|
case DUE -> {
|
||||||
if (dueInstallment != null) {
|
if (dueInstallment != null) {
|
||||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment,
|
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||||
firstNormalInstallmentNumber);
|
dueInstallment, firstNormalInstallmentNumber);
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, dueInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), dueInstallment,
|
||||||
refundedPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
|
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, dueInstallment,
|
||||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
} else {
|
} else {
|
||||||
break outerLoop;
|
context.setExitCondition(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case IN_ADVANCE -> {
|
case IN_ADVANCE -> {
|
||||||
int numberOfInstallments = inAdvanceInstallments.size();
|
int numberOfInstallments = inAdvanceInstallments.size();
|
||||||
if (numberOfInstallments > 0) {
|
if (numberOfInstallments > 0) {
|
||||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
MoneyHelper.getMathContext());
|
||||||
|
Money balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||||
|
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
|
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
|
||||||
firstNormalInstallmentNumber);
|
context.getCtx().getCharges(), inAdvanceInstallment, firstNormalInstallmentNumber);
|
||||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||||
evenPortion = evenPortion.add(balanceAdjustment);
|
evenPortion = evenPortion.add(balanceAdjustment);
|
||||||
}
|
}
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), inAdvanceInstallment,
|
||||||
refundedPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
|
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, inAdvanceInstallment,
|
||||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
context.getLoanTransaction(), evenPortion, loanTransactionToRepaymentScheduleMapping,
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
inAdvanceInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break outerLoop;
|
context.setExitCondition(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (installments.stream().anyMatch(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
});
|
||||||
&& transactionAmountUnprocessed.isGreaterThanZero());
|
return paymentAllocationContext.getTransactionAmountUnprocessed();
|
||||||
return transactionAmountUnprocessed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money refundTransactionVertically(LoanTransaction loanTransaction, TransactionCtx ctx,
|
private Money refundTransactionVertically(LoanTransaction loanTransaction, TransactionCtx ctx,
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
|
||||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances,
|
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances,
|
||||||
PaymentAllocationType paymentAllocationType) {
|
PaymentAllocationType paymentAllocationType) {
|
||||||
MonetaryCurrency currency = ctx.getCurrency();
|
VerticalPaymentAllocationContext paymentAllocationContext = new VerticalPaymentAllocationContext(ctx, loanTransaction,
|
||||||
Money zero = Money.zero(currency);
|
futureInstallmentAllocationRule, transactionMappings, balances);
|
||||||
Money refundedPortion = zero;
|
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
|
||||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
paymentAllocationContext.setPaymentAllocationType(paymentAllocationType);
|
||||||
Set<LoanCharge> charges = ctx.getCharges();
|
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size() * 100, //
|
||||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
paymentAllocationContext, //
|
||||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
(VerticalPaymentAllocationContext context) -> context.getInstallment() != null
|
||||||
do {
|
&& context.getTransactionAmountUnprocessed().isGreaterThanZero()
|
||||||
switch (paymentAllocationType.getDueType()) {
|
&& context.getAllocatedAmount().isGreaterThanZero(), //
|
||||||
|
context -> {
|
||||||
|
switch (context.getPaymentAllocationType().getDueType()) {
|
||||||
case PAST_DUE -> {
|
case PAST_DUE -> {
|
||||||
currentInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency, installments, zero);
|
context.setInstallment(getLatestPastDueInstallmentForRefund(context.getLoanTransaction(),
|
||||||
if (currentInstallment != null) {
|
context.getCtx().getCurrency(), context.getCtx().getInstallments()));
|
||||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
if (context.getInstallment() != null) {
|
||||||
firstNormalInstallmentNumber);
|
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||||
|
context.getInstallment(), context.getFirstNormalInstallmentNumber());
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
|
||||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
|
context.setAllocatedAmount(processPaymentAllocation(context.getPaymentAllocationType(),
|
||||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
context.getInstallment(), context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case DUE -> {
|
case DUE -> {
|
||||||
currentInstallment = getDueInstallmentForRefund(loanTransaction, currency, installments, zero);
|
context.setInstallment(getDueInstallmentForRefund(context.getLoanTransaction(), context.getCtx().getCurrency(),
|
||||||
if (currentInstallment != null) {
|
context.getCtx().getInstallments()));
|
||||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
if (context.getInstallment() != null) {
|
||||||
firstNormalInstallmentNumber);
|
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||||
|
context.getInstallment(), context.getFirstNormalInstallmentNumber());
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
|
||||||
refundedPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, balances,
|
context.setAllocatedAmount(processPaymentAllocation(context.getPaymentAllocationType(),
|
||||||
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
context.getInstallment(), context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(refundedPortion);
|
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case IN_ADVANCE -> {
|
case IN_ADVANCE -> {
|
||||||
List<LoanRepaymentScheduleInstallment> currentInstallments = getFutureInstallmentsForRefund(loanTransaction, currency,
|
List<LoanRepaymentScheduleInstallment> currentInstallments = getFutureInstallmentsForRefund(
|
||||||
installments, futureInstallmentAllocationRule, zero);
|
context.getLoanTransaction(), context.getCtx().getCurrency(), context.getCtx().getInstallments(),
|
||||||
|
context.getFutureInstallmentAllocationRule());
|
||||||
int numberOfInstallments = currentInstallments.size();
|
int numberOfInstallments = currentInstallments.size();
|
||||||
refundedPortion = zero;
|
context.setAllocatedAmount(Money.zero(context.getCtx().getCurrency()));
|
||||||
if (numberOfInstallments > 0) {
|
if (numberOfInstallments > 0) {
|
||||||
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
|
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||||
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
MoneyHelper.getMathContext());
|
||||||
|
Money balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||||
|
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||||
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||||
currentInstallment = internalCurrentInstallment;
|
context.setInstallment(internalCurrentInstallment);
|
||||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||||
firstNormalInstallmentNumber);
|
context.getInstallment(), context.getFirstNormalInstallmentNumber());
|
||||||
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
|
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
|
||||||
evenPortion = evenPortion.add(balanceAdjustment);
|
evenPortion = evenPortion.add(balanceAdjustment);
|
||||||
}
|
}
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
|
||||||
Money internalUnpaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
context.getCtx().getCurrency());
|
||||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges,
|
Money internalUnpaidPortion = processPaymentAllocation(context.getPaymentAllocationType(),
|
||||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
context.getInstallment(), context.getLoanTransaction(), evenPortion,
|
||||||
|
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.UNPAY);
|
||||||
if (internalUnpaidPortion.isGreaterThanZero()) {
|
if (internalUnpaidPortion.isGreaterThanZero()) {
|
||||||
refundedPortion = internalUnpaidPortion;
|
context.setAllocatedAmount(internalUnpaidPortion);
|
||||||
}
|
}
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalUnpaidPortion);
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(internalUnpaidPortion));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentInstallment = null;
|
context.setInstallment(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && refundedPortion.isGreaterThanZero());
|
});
|
||||||
return transactionAmountUnprocessed;
|
return paymentAllocationContext.getTransactionAmountUnprocessed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static LoanRepaymentScheduleInstallment getDueInstallmentForRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
private static LoanRepaymentScheduleInstallment getDueInstallmentForRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
List<LoanRepaymentScheduleInstallment> installments, Money zero) {
|
List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThanZero())
|
||||||
.filter(installment -> loanTransaction.isOn(installment.getDueDate()))
|
.filter(installment -> loanTransaction.isOn(installment.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static LoanRepaymentScheduleInstallment getLatestPastDueInstallmentForRefund(LoanTransaction loanTransaction,
|
private static LoanRepaymentScheduleInstallment getLatestPastDueInstallmentForRefund(LoanTransaction loanTransaction,
|
||||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Money zero) {
|
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
|
return installments.stream().filter(installment -> installment.getTotalPaid(currency).isGreaterThanZero())
|
||||||
.filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
.filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||||
}
|
}
|
||||||
@@ -1774,28 +1809,28 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
@NotNull
|
@NotNull
|
||||||
private static List<LoanRepaymentScheduleInstallment> getFutureInstallmentsForRefund(LoanTransaction loanTransaction,
|
private static List<LoanRepaymentScheduleInstallment> getFutureInstallmentsForRefund(LoanTransaction loanTransaction,
|
||||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
|
||||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Money zero) {
|
FutureInstallmentAllocationRule futureInstallmentAllocationRule) {
|
||||||
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
|
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
|
||||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
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();
|
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
||||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
} 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()))
|
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
} 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()))
|
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||||
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
||||||
// try to resolve as current installment ( not due )
|
// 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())
|
.filter(e -> loanTransaction.isBefore(e.getDueDate())).filter(f -> loanTransaction.isAfter(f.getFromDate())
|
||||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
||||||
.toList();
|
.toList();
|
||||||
// if there is no current installment, resolve similar to LAST_INSTALLMENT
|
// if there is no current installment, resolve similar to LAST_INSTALLMENT
|
||||||
if (inAdvanceInstallments.isEmpty()) {
|
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()))
|
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
||||||
}
|
}
|
||||||
@@ -1807,8 +1842,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
Money zero = Money.zero(transactionCtx.getCurrency());
|
Money zero = Money.zero(transactionCtx.getCurrency());
|
||||||
Balances balances = new Balances(zero, zero, zero, zero);
|
Balances balances = new Balances(zero, zero, zero, zero);
|
||||||
transactionAmountUnprocessed = processPeriods(loanTransaction, transactionAmountUnprocessed, transactionCtx.getCharges(),
|
transactionAmountUnprocessed = processPeriods(loanTransaction, transactionAmountUnprocessed, transactionMappings, balances,
|
||||||
transactionMappings, balances, transactionCtx);
|
transactionCtx);
|
||||||
|
|
||||||
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
|
||||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||||
@@ -1817,31 +1852,28 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx);
|
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) {
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances, TransactionCtx transactionCtx) {
|
||||||
LoanPaymentAllocationRule allocationRule = getAllocationRule(transaction);
|
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,
|
private Money processPeriods(LoanTransaction transaction, Money processAmount, LoanPaymentAllocationRule allocationRule,
|
||||||
Set<LoanCharge> charges, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances,
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances, TransactionCtx transactionCtx) {
|
||||||
TransactionCtx transactionCtx) {
|
|
||||||
LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail()
|
LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail()
|
||||||
.getLoanScheduleProcessingType();
|
.getLoanScheduleProcessingType();
|
||||||
if (scheduleProcessingType.isHorizontal()) {
|
if (scheduleProcessingType.isHorizontal()) {
|
||||||
return processPeriodsHorizontally(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges,
|
return processPeriodsHorizontally(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, balances);
|
||||||
balances);
|
|
||||||
}
|
}
|
||||||
if (scheduleProcessingType.isVertical()) {
|
if (scheduleProcessingType.isVertical()) {
|
||||||
return processPeriodsVertically(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges,
|
return processPeriodsVertically(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, balances);
|
||||||
balances);
|
|
||||||
}
|
}
|
||||||
return processAmount;
|
return processAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
||||||
Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule,
|
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()
|
LinkedHashMap<DueType, List<PaymentAllocationType>> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream()
|
||||||
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
|
.collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new,
|
||||||
mapping(Function.identity(), toList())));
|
mapping(Function.identity(), toList())));
|
||||||
@@ -1849,180 +1881,205 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
|
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
|
||||||
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed,
|
||||||
paymentAllocationsEntry.getValue(), paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings,
|
paymentAllocationsEntry.getValue(), paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings,
|
||||||
charges, balances);
|
balances);
|
||||||
}
|
}
|
||||||
return transactionAmountUnprocessed;
|
return transactionAmountUnprocessed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx,
|
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
||||||
Money transactionAmountUnprocessed, List<PaymentAllocationType> paymentAllocationTypes,
|
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
|
||||||
if (MathUtil.isEmpty(transactionAmountUnprocessed)) {
|
if (MathUtil.isEmpty(transactionAmountUnprocessed)) {
|
||||||
return transactionAmountUnprocessed;
|
return transactionAmountUnprocessed;
|
||||||
}
|
}
|
||||||
|
HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction,
|
||||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances);
|
||||||
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
|
||||||
Money paidPortion;
|
|
||||||
boolean exit = false;
|
|
||||||
Predicate<LoanRepaymentScheduleInstallment> inAdvanceInstallmentsFilteringRules;
|
|
||||||
boolean interestBearingAndInterestRecalculationEnabled = loanTransaction.getLoan()
|
boolean interestBearingAndInterestRecalculationEnabled = loanTransaction.getLoan()
|
||||||
.isInterestBearingAndInterestRecalculationEnabled();
|
.isInterestBearingAndInterestRecalculationEnabled();
|
||||||
boolean isProgressiveCtx = transactionCtx instanceof ProgressiveTransactionCtx;
|
boolean isProgressiveCtx = ctx instanceof ProgressiveTransactionCtx;
|
||||||
|
|
||||||
if (isProgressiveCtx && interestBearingAndInterestRecalculationEnabled) {
|
if (isProgressiveCtx && interestBearingAndInterestRecalculationEnabled) {
|
||||||
ProgressiveTransactionCtx ctx = (ProgressiveTransactionCtx) transactionCtx;
|
ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) ctx;
|
||||||
// Clear any previously skipped installments before re-evaluating
|
// Clear any previously skipped installments before re-evaluating
|
||||||
ctx.getSkipRepaymentScheduleInstallments().clear();
|
progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().clear();
|
||||||
|
paymentAllocationContext
|
||||||
inAdvanceInstallmentsFilteringRules = installment -> loanTransaction.isBefore(installment.getDueDate())
|
.setInAdvanceInstallmentsFilteringRules(installment -> loanTransaction.isBefore(installment.getDueDate())
|
||||||
&& (installment.isNotFullyPaidOff()
|
&& (installment.isNotFullyPaidOff() || (installment.isDueBalanceZero()
|
||||||
|| (installment.isDueBalanceZero() && !ctx.getSkipRepaymentScheduleInstallments().contains(installment)));
|
&& !progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().contains(installment))));
|
||||||
} else {
|
} else {
|
||||||
inAdvanceInstallmentsFilteringRules = installment -> loanTransaction.isBefore(installment.getDueDate())
|
paymentAllocationContext.setInAdvanceInstallmentsFilteringRules(
|
||||||
&& installment.isNotFullyPaidOff();
|
installment -> loanTransaction.isBefore(installment.getDueDate()) && installment.isNotFullyPaidOff());
|
||||||
}
|
}
|
||||||
|
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size() * 100, //
|
||||||
do {
|
paymentAllocationContext, //
|
||||||
LoanRepaymentScheduleInstallment oldestPastDueInstallment = installments.stream()
|
(HorizontalPaymentAllocationContext context) -> !context.isExitCondition()
|
||||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
&& 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);
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||||
LoanRepaymentScheduleInstallment dueInstallment = installments.stream()
|
LoanRepaymentScheduleInstallment dueInstallment = context.getCtx().getInstallments().stream()
|
||||||
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
||||||
|
.filter(e -> context.getLoanTransaction().isOn(e.getDueDate()))
|
||||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
||||||
|
|
||||||
// For having similar logic we are populating installment list even when the future installment
|
// 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.
|
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
|
||||||
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
|
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
|
||||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(context.getFutureInstallmentAllocationRule())) {
|
||||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
inAdvanceInstallments = context.getCtx().getInstallments().stream()
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
|
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
||||||
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())) //
|
||||||
.toList(); //
|
.toList(); //
|
||||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule())) {
|
||||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
inAdvanceInstallments = context.getCtx().getInstallments().stream()
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
|
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
||||||
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())) //
|
||||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
||||||
.toList(); //
|
.toList(); //
|
||||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule())) {
|
||||||
inAdvanceInstallments = installments.stream().filter(inAdvanceInstallmentsFilteringRules)
|
inAdvanceInstallments = context.getCtx().getInstallments().stream()
|
||||||
|
.filter(context.getInAdvanceInstallmentsFilteringRules())
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
||||||
.toList(); //
|
.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 )
|
// try to resolve as current installment ( not due )
|
||||||
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
inAdvanceInstallments = context.getCtx().getInstallments().stream()
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())) //
|
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
|
||||||
.filter(f -> loanTransaction.isAfter(f.getFromDate())
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())) //
|
||||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1)) //
|
.filter(f -> context.getLoanTransaction().isAfter(f.getFromDate())
|
||||||
|
|| (context.getLoanTransaction().isOn(f.getFromDate()) && f.getInstallmentNumber() == 1)) //
|
||||||
.toList(); //
|
.toList(); //
|
||||||
// if there is no current installment, resolve similar to LAST_INSTALLMENT
|
// if there is no current installment, resolve similar to LAST_INSTALLMENT
|
||||||
if (inAdvanceInstallments.isEmpty()) {
|
if (inAdvanceInstallments.isEmpty()) {
|
||||||
inAdvanceInstallments = installments.stream().filter(inAdvanceInstallmentsFilteringRules)
|
inAdvanceInstallments = context.getCtx().getInstallments().stream()
|
||||||
|
.filter(context.getInAdvanceInstallmentsFilteringRules())
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
|
||||||
.toList(); //
|
.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()) {
|
switch (paymentAllocationType.getDueType()) {
|
||||||
case PAST_DUE -> {
|
case PAST_DUE -> {
|
||||||
if (oldestPastDueInstallment != null) {
|
if (oldestPastDueInstallment != null) {
|
||||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, oldestPastDueInstallment,
|
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
|
||||||
firstNormalInstallmentNumber);
|
context.getCtx().getCharges(), oldestPastDueInstallment, firstNormalInstallmentNumber);
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, oldestPastDueInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), oldestPastDueInstallment,
|
||||||
Loan loan = loanTransaction.getLoan();
|
context.getCtx().getCurrency());
|
||||||
if (transactionCtx instanceof ProgressiveTransactionCtx ctx
|
Loan loan = context.getLoanTransaction().getLoan();
|
||||||
&& loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) {
|
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||||
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction,
|
&& loan.isInterestBearingAndInterestRecalculationEnabled()
|
||||||
transactionAmountUnprocessed, balances, paymentAllocationType, oldestPastDueInstallment, ctx,
|
&& !progressiveTransactionCtx.isChargedOff()) {
|
||||||
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges);
|
context.setAllocatedAmount(
|
||||||
|
handlingPaymentAllocationForInterestBearingProgressiveLoan(context.getLoanTransaction(),
|
||||||
|
context.getTransactionAmountUnprocessed(), context.getBalances(),
|
||||||
|
paymentAllocationType, oldestPastDueInstallment, progressiveTransactionCtx,
|
||||||
|
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges));
|
||||||
} else {
|
} else {
|
||||||
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
|
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment,
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
|
||||||
|
context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||||
}
|
}
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
} else {
|
} else {
|
||||||
exit = true;
|
context.setExitCondition(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case DUE -> {
|
case DUE -> {
|
||||||
if (dueInstallment != null) {
|
if (dueInstallment != null) {
|
||||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, dueInstallment,
|
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||||
firstNormalInstallmentNumber);
|
dueInstallment, firstNormalInstallmentNumber);
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, dueInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), dueInstallment,
|
||||||
Loan loan = loanTransaction.getLoan();
|
context.getCtx().getCurrency());
|
||||||
if (transactionCtx instanceof ProgressiveTransactionCtx ctx
|
Loan loan = context.getLoanTransaction().getLoan();
|
||||||
&& loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) {
|
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||||
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction,
|
&& loan.isInterestBearingAndInterestRecalculationEnabled()
|
||||||
transactionAmountUnprocessed, balances, paymentAllocationType, dueInstallment, ctx,
|
&& !progressiveTransactionCtx.isChargedOff()) {
|
||||||
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges);
|
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
|
||||||
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
|
context.getBalances(), paymentAllocationType, dueInstallment, progressiveTransactionCtx,
|
||||||
|
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges));
|
||||||
} else {
|
} else {
|
||||||
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
|
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, dueInstallment,
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||||
}
|
}
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
} else {
|
} else {
|
||||||
exit = true;
|
context.setExitCondition(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case IN_ADVANCE -> {
|
case IN_ADVANCE -> {
|
||||||
int numberOfInstallments = inAdvanceInstallments.size();
|
int numberOfInstallments = inAdvanceInstallments.size();
|
||||||
if (numberOfInstallments > 0) {
|
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
|
// 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
|
// 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()) {
|
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.
|
// transaction amount.
|
||||||
evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments,
|
evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
|
||||||
new MathContext(MoneyHelper.getMathContext().getPrecision(), RoundingMode.DOWN));
|
new MathContext(MoneyHelper.getMathContext().getPrecision(), RoundingMode.DOWN));
|
||||||
balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
|
balanceAdjustment = context.getTransactionAmountUnprocessed()
|
||||||
|
.minus(evenPortion.multipliedBy(numberOfInstallments));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
|
||||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
|
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
|
||||||
firstNormalInstallmentNumber);
|
context.getCtx().getCharges(), inAdvanceInstallment, firstNormalInstallmentNumber);
|
||||||
|
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
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
|
// Adjust the portion for the last installment
|
||||||
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
|
||||||
evenPortion = evenPortion.add(balanceAdjustment);
|
evenPortion = evenPortion.add(balanceAdjustment);
|
||||||
}
|
}
|
||||||
if (transactionCtx instanceof ProgressiveTransactionCtx ctx
|
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
|
||||||
&& loan.isInterestBearingAndInterestRecalculationEnabled() && !ctx.isChargedOff()) {
|
&& loan.isInterestBearingAndInterestRecalculationEnabled()
|
||||||
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, evenPortion,
|
&& !progressiveTransactionCtx.isChargedOff()) {
|
||||||
balances, paymentAllocationType, inAdvanceInstallment, ctx,
|
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
|
||||||
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges);
|
context.getLoanTransaction(), evenPortion, context.getBalances(), paymentAllocationType,
|
||||||
|
inAdvanceInstallment, progressiveTransactionCtx,
|
||||||
|
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges));
|
||||||
} else {
|
} else {
|
||||||
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
|
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, inAdvanceInstallment,
|
||||||
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
|
context.getLoanTransaction(), evenPortion, loanTransactionToRepaymentScheduleMapping,
|
||||||
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
inAdvanceInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||||
}
|
}
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
exit = true;
|
context.setExitCondition(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
|
return paymentAllocationContext.getTransactionAmountUnprocessed();
|
||||||
// or there is no more outstanding balance of the allocation type
|
|
||||||
while (!exit && installments.stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
|
|
||||||
&& transactionAmountUnprocessed.isGreaterThanZero());
|
|
||||||
return transactionAmountUnprocessed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTransaction loanTransaction,
|
private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTransaction loanTransaction,
|
||||||
@@ -2115,115 +2172,146 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
|
|
||||||
private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
||||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||||
Set<LoanCharge> charges, Balances balances) {
|
Balances balances) {
|
||||||
MonetaryCurrency currency = ctx.getCurrency();
|
VerticalPaymentAllocationContext paymentAllocationContext = new VerticalPaymentAllocationContext(ctx, loanTransaction,
|
||||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, balances);
|
||||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
|
||||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
|
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
|
||||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
paymentAllocationContext.setAllocatedAmount(Money.zero(ctx.getCurrency()));
|
||||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
paymentAllocationContext.setInstallment(null);
|
||||||
Money paidPortion = Money.zero(currency);
|
paymentAllocationContext.setPaymentAllocationType(paymentAllocationType);
|
||||||
do {
|
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size() * 100, //
|
||||||
Predicate<LoanRepaymentScheduleInstallment> predicate = getFilterPredicate(paymentAllocationType, currency);
|
paymentAllocationContext, //
|
||||||
switch (paymentAllocationType.getDueType()) {
|
(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 -> {
|
case PAST_DUE -> {
|
||||||
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isAfter(e.getDueDate()))
|
context.setInstallment(context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
.filter(e -> context.getLoanTransaction().isAfter(e.getDueDate()))
|
||||||
if (currentInstallment != null) {
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null));
|
||||||
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
if (context.getInstallment() != null) {
|
||||||
firstNormalInstallmentNumber);
|
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
|
||||||
|
context.getCtx().getCharges(), context.getInstallment(),
|
||||||
|
context.getFirstNormalInstallmentNumber());
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
|
||||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
|
context.setAllocatedAmount(
|
||||||
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
processPaymentAllocation(context.getPaymentAllocationType(), context.getInstallment(),
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
|
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
|
||||||
|
context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case DUE -> {
|
case DUE -> {
|
||||||
currentInstallment = installments.stream().filter(predicate).filter(e -> loanTransaction.isOn(e.getDueDate()))
|
context.setInstallment(context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
|
.filter(e -> context.getLoanTransaction().isOn(e.getDueDate()))
|
||||||
if (currentInstallment != null) {
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null));
|
||||||
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
if (context.getInstallment() != null) {
|
||||||
firstNormalInstallmentNumber);
|
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
|
||||||
|
context.getInstallment(), context.getFirstNormalInstallmentNumber());
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
|
||||||
paidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment, loanTransaction,
|
context.getCtx().getCurrency());
|
||||||
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
|
context.setAllocatedAmount(
|
||||||
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
processPaymentAllocation(context.getPaymentAllocationType(), context.getInstallment(),
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
|
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
|
||||||
|
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
|
||||||
|
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
|
||||||
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case IN_ADVANCE -> {
|
case IN_ADVANCE -> {
|
||||||
// For having similar logic we are populating installment list even when the future installment
|
// For having similar logic we are populating installment list even when the future
|
||||||
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
|
// installment
|
||||||
|
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one
|
||||||
|
// element.
|
||||||
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
|
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
|
||||||
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
|
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(context.getFutureInstallmentAllocationRule())) {
|
||||||
currentInstallments = installments.stream().filter(predicate)
|
currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())).toList();
|
||||||
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT
|
||||||
currentInstallments = installments.stream().filter(predicate)
|
.equals(context.getFutureInstallmentAllocationRule())) {
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
|
||||||
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
|
||||||
currentInstallments = installments.stream().filter(predicate)
|
.toList();
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
|
.equals(context.getFutureInstallmentAllocationRule())) {
|
||||||
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
|
currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
// get current installment where from date < transaction date < to date OR transaction date
|
.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 )
|
// is on first installment's first day ( from day )
|
||||||
currentInstallments = installments.stream().filter(predicate)
|
currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
|
||||||
.filter(f -> loanTransaction.isAfter(f.getFromDate())
|
.filter(f -> context.getLoanTransaction().isAfter(f.getFromDate())
|
||||||
|| (loanTransaction.isOn(f.getFromDate()) && f.getInstallmentNumber() == 1))
|
|| (context.getLoanTransaction().isOn(f.getFromDate())
|
||||||
|
&& f.getInstallmentNumber() == 1))
|
||||||
.toList();
|
.toList();
|
||||||
// if there is no current in advance installment resolve similar to LAST_INSTALLMENT
|
// if there is no current in advance installment resolve similar to LAST_INSTALLMENT
|
||||||
if (currentInstallments.isEmpty()) {
|
if (currentInstallments.isEmpty()) {
|
||||||
currentInstallments = installments.stream().filter(predicate)
|
currentInstallments = context.getCtx().getInstallments().stream().filter(predicate)
|
||||||
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
|
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate()))
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream()
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int numberOfInstallments = currentInstallments.size();
|
int numberOfInstallments = currentInstallments.size();
|
||||||
paidPortion = Money.zero(currency);
|
context.setAllocatedAmount(Money.zero(context.getCtx().getCurrency()));
|
||||||
if (numberOfInstallments > 0) {
|
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
|
// 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
|
// 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) {
|
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
|
||||||
currentInstallment = internalCurrentInstallment;
|
context.setInstallment(internalCurrentInstallment);
|
||||||
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, currentInstallment,
|
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
|
||||||
firstNormalInstallmentNumber);
|
context.getCtx().getCharges(), context.getInstallment(),
|
||||||
|
context.getFirstNormalInstallmentNumber());
|
||||||
// Adjust the portion for the last installment
|
// Adjust the portion for the last installment
|
||||||
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
|
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
|
||||||
evenPortion = evenPortion.add(balanceAdjustment);
|
evenPortion = evenPortion.add(balanceAdjustment);
|
||||||
}
|
}
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
transactionMappings, loanTransaction, currentInstallment, currency);
|
context.getTransactionMappings(), context.getLoanTransaction(), context.getInstallment(),
|
||||||
Money internalPaidPortion = processPaymentAllocation(paymentAllocationType, currentInstallment,
|
context.getCtx().getCurrency());
|
||||||
loanTransaction, evenPortion, loanTransactionToRepaymentScheduleMapping,
|
Money internalPaidPortion = processPaymentAllocation(context.getPaymentAllocationType(),
|
||||||
inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
|
context.getInstallment(), context.getLoanTransaction(), evenPortion,
|
||||||
// Some extra logic to allocate as much as possible across the installments if the
|
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
|
// outstanding balances are different
|
||||||
if (internalPaidPortion.isGreaterThanZero()) {
|
if (internalPaidPortion.isGreaterThanZero()) {
|
||||||
paidPortion = internalPaidPortion;
|
context.setAllocatedAmount(internalPaidPortion);
|
||||||
}
|
}
|
||||||
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalPaidPortion);
|
context.setTransactionAmountUnprocessed(
|
||||||
|
context.getTransactionAmountUnprocessed().minus(internalPaidPortion));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentInstallment = null;
|
context.setInstallment(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
|
return paymentAllocationContext.getTransactionAmountUnprocessed();
|
||||||
// or there is no more outstanding balance of the allocation type
|
|
||||||
while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero());
|
|
||||||
}
|
|
||||||
return transactionAmountUnprocessed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<LoanRepaymentScheduleInstallment> getFilterPredicate(PaymentAllocationType paymentAllocationType,
|
private Predicate<LoanRepaymentScheduleInstallment> getFilterPredicate(PaymentAllocationType paymentAllocationType,
|
||||||
@@ -2454,4 +2542,63 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
ctx.getChangedTransactionDetail().addNewTransactionChangeBeforeExistingOne(new TransactionChangeData(null, newAccrualTransaction),
|
ctx.getChangedTransactionDetail().addNewTransactionChangeBeforeExistingOne(new TransactionChangeData(null, newAccrualTransaction),
|
||||||
chargeOffTransaction);
|
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