private Money processAllocationsHorizontally()

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