in fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java [1913:2107]
private Money processAllocationsHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
if (MathUtil.isEmpty(transactionAmountUnprocessed)) {
return transactionAmountUnprocessed;
}
HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction,
paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances);
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
boolean interestBearingAndInterestRecalculationEnabled = loanTransaction.getLoan()
.isInterestBearingAndInterestRecalculationEnabled();
boolean isProgressiveCtx = ctx instanceof ProgressiveTransactionCtx;
if (isProgressiveCtx && interestBearingAndInterestRecalculationEnabled) {
ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) ctx;
// Clear any previously skipped installments before re-evaluating
progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().clear();
paymentAllocationContext
.setInAdvanceInstallmentsFilteringRules(installment -> loanTransaction.isBefore(installment.getDueDate())
&& (installment.isNotFullyPaidOff() || (installment.isDueBalanceZero()
&& !progressiveTransactionCtx.getSkipRepaymentScheduleInstallments().contains(installment))));
} else {
paymentAllocationContext.setInAdvanceInstallmentsFilteringRules(
installment -> loanTransaction.isBefore(installment.getDueDate()) && installment.isNotFullyPaidOff());
}
LoopGuard.runSafeDoWhileLoop(paymentAllocationContext.getCtx().getInstallments().size() * 100, //
paymentAllocationContext, //
(HorizontalPaymentAllocationContext context) -> !context.isExitCondition()
&& context.getCtx().getInstallments().stream().anyMatch(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
&& context.getTransactionAmountUnprocessed().isGreaterThanZero(), //
context -> {
LoanRepaymentScheduleInstallment oldestPastDueInstallment = context.getCtx().getInstallments().stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> context.getLoanTransaction().isAfter(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
LoanRepaymentScheduleInstallment dueInstallment = context.getCtx().getInstallments().stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> context.getLoanTransaction().isOn(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
// For having similar logic we are populating installment list even when the future installment
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(context.getFutureInstallmentAllocationRule())) {
inAdvanceInstallments = context.getCtx().getInstallments().stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())) //
.toList(); //
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule())) {
inAdvanceInstallments = context.getCtx().getInstallments().stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())) //
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
.toList(); //
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule())) {
inAdvanceInstallments = context.getCtx().getInstallments().stream()
.filter(context.getInAdvanceInstallmentsFilteringRules())
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
.toList(); //
} else if (FutureInstallmentAllocationRule.NEXT_LAST_INSTALLMENT.equals(context.getFutureInstallmentAllocationRule())) {
// try to resolve as current installment ( not due )
inAdvanceInstallments = context.getCtx().getInstallments().stream()
.filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) //
.filter(e -> context.getLoanTransaction().isBefore(e.getDueDate())) //
.filter(f -> context.getLoanTransaction().isAfter(f.getFromDate())
|| (context.getLoanTransaction().isOn(f.getFromDate()) && f.getInstallmentNumber() == 1)) //
.toList(); //
// if there is no current installment, resolve similar to LAST_INSTALLMENT
if (inAdvanceInstallments.isEmpty()) {
inAdvanceInstallments = context.getCtx().getInstallments().stream()
.filter(context.getInAdvanceInstallmentsFilteringRules())
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream() //
.toList(); //
}
}
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper
.fetchFirstNormalInstallmentNumber(context.getCtx().getInstallments());
for (PaymentAllocationType paymentAllocationType : context.getPaymentAllocationTypes()) {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> {
if (oldestPastDueInstallment != null) {
Set<LoanCharge> oldestPastDueInstallmentCharges = getLoanChargesOfInstallment(
context.getCtx().getCharges(), oldestPastDueInstallment, firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
context.getTransactionMappings(), context.getLoanTransaction(), oldestPastDueInstallment,
context.getCtx().getCurrency());
Loan loan = context.getLoanTransaction().getLoan();
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()
&& !progressiveTransactionCtx.isChargedOff()) {
context.setAllocatedAmount(
handlingPaymentAllocationForInterestBearingProgressiveLoan(context.getLoanTransaction(),
context.getTransactionAmountUnprocessed(), context.getBalances(),
paymentAllocationType, oldestPastDueInstallment, progressiveTransactionCtx,
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges));
} else {
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment,
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges,
context.getBalances(), LoanRepaymentScheduleInstallment.PaymentAction.PAY));
}
context.setTransactionAmountUnprocessed(
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
} else {
context.setExitCondition(true);
}
}
case DUE -> {
if (dueInstallment != null) {
Set<LoanCharge> dueInstallmentCharges = getLoanChargesOfInstallment(context.getCtx().getCharges(),
dueInstallment, firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
context.getTransactionMappings(), context.getLoanTransaction(), dueInstallment,
context.getCtx().getCurrency());
Loan loan = context.getLoanTransaction().getLoan();
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()
&& !progressiveTransactionCtx.isChargedOff()) {
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
context.getBalances(), paymentAllocationType, dueInstallment, progressiveTransactionCtx,
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges));
} else {
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, dueInstallment,
context.getLoanTransaction(), context.getTransactionAmountUnprocessed(),
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges, context.getBalances(),
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
}
context.setTransactionAmountUnprocessed(
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
} else {
context.setExitCondition(true);
}
}
case IN_ADVANCE -> {
int numberOfInstallments = inAdvanceInstallments.size();
if (numberOfInstallments > 0) {
// This will be the same amount as transactionAmountUnprocessed in case of the
// future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
Money evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
MoneyHelper.getMathContext());
// Adjustment might be needed due to the divide operation and the rounding mode
Money balanceAdjustment = context.getTransactionAmountUnprocessed()
.minus(evenPortion.multipliedBy(numberOfInstallments));
if (evenPortion.add(balanceAdjustment).isLessThanZero()) {
// Note: Rounding mode DOWN grants that evenPortion cant pay more than
// unprocessed
// transaction amount.
evenPortion = context.getTransactionAmountUnprocessed().dividedBy(numberOfInstallments,
new MathContext(MoneyHelper.getMathContext().getPrecision(), RoundingMode.DOWN));
balanceAdjustment = context.getTransactionAmountUnprocessed()
.minus(evenPortion.multipliedBy(numberOfInstallments));
}
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(
context.getCtx().getCharges(), inAdvanceInstallment, firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
context.getTransactionMappings(), context.getLoanTransaction(), inAdvanceInstallment,
context.getCtx().getCurrency());
Loan loan = context.getLoanTransaction().getLoan();
// Adjust the portion for the last installment
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment);
}
if (context.getCtx() instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()
&& !progressiveTransactionCtx.isChargedOff()) {
context.setAllocatedAmount(handlingPaymentAllocationForInterestBearingProgressiveLoan(
context.getLoanTransaction(), evenPortion, context.getBalances(), paymentAllocationType,
inAdvanceInstallment, progressiveTransactionCtx,
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges));
} else {
context.setAllocatedAmount(processPaymentAllocation(paymentAllocationType, inAdvanceInstallment,
context.getLoanTransaction(), evenPortion, loanTransactionToRepaymentScheduleMapping,
inAdvanceInstallmentCharges, context.getBalances(),
LoanRepaymentScheduleInstallment.PaymentAction.PAY));
}
context.setTransactionAmountUnprocessed(
context.getTransactionAmountUnprocessed().minus(context.getAllocatedAmount()));
}
} else {
context.setExitCondition(true);
}
}
}
}
});
return paymentAllocationContext.getTransactionAmountUnprocessed();
}