diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java index aa1351880b3..265cc6a34fd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java @@ -46,20 +46,24 @@ public Money getTotalOutstanding() { .plus(penaltyCharges()); } - public void plusPrincipal(Money principal) { + public OutstandingAmountsDTO plusPrincipal(Money principal) { this.principal = this.principal.plus(principal); + return this; } - public void plusInterest(Money interest) { + public OutstandingAmountsDTO plusInterest(Money interest) { this.interest = this.interest.plus(interest); + return this; } - public void plusFeeCharges(Money feeCharges) { + public OutstandingAmountsDTO plusFeeCharges(Money feeCharges) { this.feeCharges = this.feeCharges.plus(feeCharges); + return this; } - public void plusPenaltyCharges(Money penaltyCharges) { + public OutstandingAmountsDTO plusPenaltyCharges(Money penaltyCharges) { this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges); + return this; } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index a9b5773b94e..3d9512c0091 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -55,6 +55,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -77,6 +78,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -143,12 +145,13 @@ public Money handleRepaymentSchedule(List transactionsPostDisbu throw new NotImplementedException(); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { + // only for progressive loans + public Pair reprocessProgressiveLoanTransactions( + LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, + List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, null); } if (charges != null) { for (final LoanCharge loanCharge : charges) { @@ -185,10 +188,18 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement chargeOrTransaction.getLoanCharge() .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); } - List txs = chargeOrTransactions.stream().map(ChargeOrTransaction::getLoanTransaction).filter(Optional::isPresent) + List txs = chargeOrTransactions.stream() // + .map(ChargeOrTransaction::getLoanTransaction) // + .filter(Optional::isPresent) // .map(Optional::get).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, scheduleModel); + } + + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); } @Override @@ -281,7 +292,7 @@ protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransactio } return originalTransaction.get(); } else { // when there is no id, then it might be that the original transaction is changed, so we need to look - // it up from the Ctx. + // it up from the Ctx. Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction); Collection updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values(); Optional updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations() @@ -738,19 +749,18 @@ private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() .filter(e -> DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); - Money transactionAmountUnprocessed = null; + Money transactionAmountUnprocessed = transactionCtx.getOverpaymentHolder().getMoneyObject(); Money zero = Money.zero(transactionCtx.getCurrency()); Balances balances = new Balances(zero, zero, zero, zero); if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(), - transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(), + transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings, Set.of(), balances); } else if (LoanScheduleProcessingType.VERTICAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(), - transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(), - defaultPaymentAllocationRule, transactionMappings, Set.of(), balances); + transactionCtx.getInstallments(), transactionAmountUnprocessed, defaultPaymentAllocationRule, transactionMappings, + Set.of(), balances); } transactionCtx.getOverpaymentHolder().setMoneyObject(transactionAmountUnprocessed); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); @@ -1109,9 +1119,8 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx if (LoanScheduleProcessingType.HORIZONTAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { - transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(), - transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings, - transactionCtx.getCharges(), balances); + transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed, + paymentAllocationRule, transactionMappings, transactionCtx.getCharges(), balances); } else if (LoanScheduleProcessingType.VERTICAL .equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) { transactionAmountUnprocessed = processPeriodsVertically(loanTransaction, transactionCtx.getCurrency(), @@ -1125,26 +1134,31 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder()); } - private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - LoanPaymentAllocationRule paymentAllocationRule, List transactionMappings, - Set charges, Balances balances) { + private Money processPeriodsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx, + Money transactionAmountUnprocessed, LoanPaymentAllocationRule paymentAllocationRule, + List transactionMappings, Set charges, Balances balances) { LinkedHashMap> paymentAllocationsMap = paymentAllocationRule.getAllocationTypes().stream() .collect(Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList()))); for (Map.Entry> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) { - transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments, - transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), - paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances); + transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, transactionCtx, transactionAmountUnprocessed, + paymentAllocationsEntry.getValue(), paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, + charges, balances); } return transactionAmountUnprocessed; } - private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, - List paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule, + private Money processAllocationsHorizontally(LoanTransaction loanTransaction, TransactionCtx transactionCtx, + Money transactionAmountUnprocessed, List paymentAllocationTypes, + FutureInstallmentAllocationRule futureInstallmentAllocationRule, List transactionMappings, Set charges, Balances balances) { + if (transactionAmountUnprocessed.isZero()) { + return transactionAmountUnprocessed; + } + + MonetaryCurrency currency = transactionCtx.getCurrency(); + List installments = transactionCtx.getInstallments(); Money paidPortion; boolean exit = false; do { @@ -1214,15 +1228,55 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) { Set inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment, firstNormalInstallmentNumber); - // Adjust the portion for the last installment - if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { - evenPortion = evenPortion.add(balanceAdjustment); - } + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( transactionMappings, loanTransaction, inAdvanceInstallment, currency); - paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, - evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, - LoanRepaymentScheduleInstallment.PaymentAction.PAY); + + Loan loan = loanTransaction.getLoan(); + if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing() + && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { + ProgressiveLoanInterestScheduleModel model = ctx.getModel(); + LocalDate transactionDate = loanTransaction.getTransactionDate(); + LocalDate payDate = inAdvanceInstallment.getFromDate().isAfter(transactionDate) + ? inAdvanceInstallment.getFromDate() + : transactionDate; + ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator + .getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate).orElseThrow(); + + switch (paymentAllocationType) { + case IN_ADVANCE_INTEREST -> + inAdvanceInstallment.updateInterestCharged(payableDetails.getInterestDue().getAmount()); + case IN_ADVANCE_PRINCIPAL -> + inAdvanceInstallment.updatePrincipal(payableDetails.getPrincipalDue().getAmount()); + default -> { + } + } + + paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, + inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY); + + switch (paymentAllocationType) { + case IN_ADVANCE_PRINCIPAL -> { + emiCalculator.addBalanceCorrection(model, payDate, + payableDetails.getOutstandingBalance().multipliedBy(-1)); + emiCalculator.addBalanceCorrection(model, payDate, + payableDetails.getPrincipalDue().minus(paidPortion)); + } + case IN_ADVANCE_INTEREST -> emiCalculator.addBalanceCorrection(model, payDate, + payableDetails.getInterestDue().minus(paidPortion)); + default -> { + } + } + } else { + // Adjust the portion for the last installment + if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) { + evenPortion = evenPortion.add(balanceAdjustment); + } + paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction, + evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances, + LoanRepaymentScheduleInstallment.PaymentAction.PAY); + } transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); } } else { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index adfd1f1b78d..b1250fd457d 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -18,25 +18,23 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; -import static java.time.temporal.ChronoUnit.DAYS; +import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE; import java.math.BigDecimal; import java.math.MathContext; -import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; @@ -45,6 +43,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; @@ -103,11 +102,15 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms); + final ArrayList disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); + disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); + for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) { scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate()); scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate()); - processDisbursements(loanApplicationTerms, scheduleParams, interestScheduleModel, periods, chargesDueAtTimeOfDisbursement); + processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods, + chargesDueAtTimeOfDisbursement); repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber()); for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { @@ -142,7 +145,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer } if (loanApplicationTerms.isMultiDisburseLoan()) { - processDisbursements(loanApplicationTerms, scheduleParams, null, periods, chargesDueAtTimeOfDisbursement); + processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, null, periods, chargesDueAtTimeOfDisbursement); } // determine fees and penalties for charges which depends on total @@ -169,11 +172,12 @@ private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTer } } - private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams scheduleParams, + private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, + final ArrayList disbursementDataList, final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List periods, final BigDecimal chargesDueAtTimeOfDisbursement) { - for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { + for (DisbursementData disbursementData : disbursementDataList) { final LocalDate disbursementDate = disbursementData.disbursementDate(); final LocalDate periodFromDate = scheduleParams.getPeriodStartDate(); final LocalDate periodDueDate = scheduleParams.getActualRepaymentDate(); @@ -241,77 +245,38 @@ public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicatio public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - return switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { - case TILL_PRE_CLOSURE_DATE -> { - log.debug("calculating prepayment amount till pre closure date (Strategy A)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - AtomicBoolean firstAfterPayoff = new AtomicBoolean(true); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isInstallmentAfterPayoff = installment.getDueDate().isAfter(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isInstallmentAfterPayoff) { - if (firstAfterPayoff.getAndSet(false)) { - outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment, onDate)); - } else { - log.debug("Installment {} - {} is after payoff, not counting interest", installment.getFromDate(), - installment.getDueDate()); - } - } else { - log.debug("adding interest for {} - {}: {}", installment.getFromDate(), installment.getDueDate(), - installment.getInterestOutstanding(currency)); - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - yield outstandingAmounts; - } - - case TILL_REST_FREQUENCY_DATE -> { - log.debug("calculating prepayment amount till rest frequency date (Strategy B)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isPayoffBeforeInstallment = installment.getFromDate().isBefore(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isPayoffBeforeInstallment) { - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } else { - log.debug("Payoff after installment {}, not counting interest", installment.getDueDate()); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - - yield outstandingAmounts; - } - case NONE -> throw new UnsupportedOperationException("Pre-closure interest calculation strategy not supported"); + List installments = loan.getRepaymentScheduleInstallments(); + + LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { + case TILL_PRE_CLOSURE_DATE -> onDate; + case TILL_REST_FREQUENCY_DATE -> // find due date of current installment + installments.stream().filter(it -> it.getFromDate().isBefore(onDate) && it.getDueDate().isAfter(onDate)).findFirst() + .orElseThrow(() -> new IllegalStateException("No installment found for transaction date: " + onDate)).getDueDate(); + case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); }; - } - private Money calculatePayableInterest(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate onDate) { - RoundingMode roundingMode = MoneyHelper.getRoundingMode(); - MonetaryCurrency currency = loan.getCurrency(); - Money originalInterest = installment.getInterestCharged(currency); - log.debug("calculating interest for {} from {} to {}", originalInterest, installment.getFromDate(), installment.getDueDate()); - - LocalDate start = installment.getFromDate(); - Money payableInterest = Money.zero(currency); - - while (!start.isEqual(onDate)) { - long between = DAYS.between(start, installment.getDueDate()); - Money dailyInterest = originalInterest.minus(payableInterest).dividedBy(between, roundingMode); - log.debug("Daily interest is {}: {} / {}, total: {}", dailyInterest, originalInterest.minus(payableInterest), between, - payableInterest.add(dailyInterest)); - payableInterest = payableInterest.add(dailyInterest); - start = start.plusDays(1); + if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); } + ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), + loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); + + LoanRepaymentScheduleInstallment actualInstallment = installments.stream() + .filter(it -> transactionDate.isAfter(it.getFromDate()) && !transactionDate.isAfter(it.getDueDate())).findFirst() + .orElse(installments.get(0)); + + ProgressiveLoanInterestRepaymentModel result = emiCalculator + .getPayableDetails(model, actualInstallment.getDueDate(), transactionDate).orElseThrow(); + + OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // + .principal(result.getOutstandingBalance()) // + .interest(result.getInterestDue()); - payableInterest = payableInterest.minus(installment.getInterestPaid(currency).minus(installment.getInterestWaived(currency))); + installments.forEach(installment -> amounts // + .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) + .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); - log.debug("Payable interest is {}", payableInterest); - return payableInterest; + return amounts; } // Private, internal methods diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java deleted file mode 100644 index 214bff051c9..00000000000 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java +++ /dev/null @@ -1,192 +0,0 @@ -/** - * 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.portfolio.loanaccount.loanschedule.domain; - -import static java.math.BigDecimal.ZERO; -import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE; -import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_REST_FREQUENCY_DATE; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.time.LocalDate; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; -import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; -import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; -import org.springframework.test.util.ReflectionTestUtils; - -class ProgressiveLoanScheduleGeneratorTest { - - static class TestRow { - - LocalDate fromDate; - LocalDate dueDate; - BigDecimal balance; - BigDecimal principal; - BigDecimal interest; - BigDecimal fee; - BigDecimal penalty; - boolean paid; - - TestRow(LocalDate fromDate, LocalDate dueDate, BigDecimal balance, BigDecimal principal, BigDecimal interest, BigDecimal fee, - BigDecimal penalty, boolean paid) { - this.fromDate = fromDate; - this.dueDate = dueDate; - this.balance = balance; - this.principal = principal; - this.interest = interest; - this.fee = fee; - this.penalty = penalty; - this.paid = paid; - } - } - - private ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(null, null); - private MonetaryCurrency usd = new MonetaryCurrency("USD", 2, null); - private HolidayDetailDTO holidays = new HolidayDetailDTO(false, null, null); - LoanRepaymentScheduleTransactionProcessor processor = mock(LoanRepaymentScheduleTransactionProcessor.class); - - static { - ConfigurationDomainService domainService = mock(ConfigurationDomainService.class); - when(domainService.getRoundingMode()).thenReturn(RoundingMode.HALF_UP.ordinal()); - ReflectionTestUtils.setField(MoneyHelper.class, "staticConfigurationDomainService", domainService); - } - - @BeforeAll - public static void beforeAll() { - ((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(ch.qos.logback.classic.Level.DEBUG); - } - - @AfterAll - public static void afterAll() { - ((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(Logger.ROOT_LOGGER_NAME).setLevel(Level.INFO); - } - - public List testRows() { - return List.of( - new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(83.57), BigDecimal.valueOf(16.43), - BigDecimal.valueOf(0.58), ZERO, ZERO, true), - new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(67.05), BigDecimal.valueOf(16.52), - BigDecimal.valueOf(0.49), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), BigDecimal.valueOf(50.43), BigDecimal.valueOf(16.62), - BigDecimal.valueOf(0.39), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), BigDecimal.valueOf(33.71), BigDecimal.valueOf(16.72), - BigDecimal.valueOf(0.29), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), BigDecimal.valueOf(16.90), BigDecimal.valueOf(16.81), - BigDecimal.valueOf(0.20), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), BigDecimal.valueOf(00.90), BigDecimal.valueOf(16.90), - BigDecimal.valueOf(0.10), ZERO, ZERO, false)); - } - - @Test - public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE); - Loan loan = prepareLoanWithInstallments(testRows()); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(83.84), amounts.getTotalOutstanding().getAmount()); - } - - @Test - public void calculatePrepaymentAmount_TILL_REST_FREQUENCY_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE); - Loan loan = prepareLoanWithInstallments(testRows()); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(84.06), amounts.getTotalOutstanding().getAmount()); - } - - @Test - public void calculateSameDayPayoff_TILL_PRE_CLOSURE_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_PRE_CLOSURE_DATE); - - Loan loan = prepareLoanWithInstallments(List.of( - new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false))); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(200.0).longValue(), amounts.getTotalOutstanding().getAmount().longValue()); - } - - @Test - public void calculateSameDayPayoff_TILL_REST_FREQUENCY_DATE() { - LoanApplicationTerms terms = mock(LoanApplicationTerms.class); - when(terms.getPreClosureInterestCalculationStrategy()).thenReturn(TILL_REST_FREQUENCY_DATE); - - Loan loan = prepareLoanWithInstallments(List.of( - new TestRow(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false), - new TestRow(LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(102), BigDecimal.valueOf(100), - BigDecimal.valueOf(2), ZERO, ZERO, false))); - - OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 1, 1), terms, MathContext.DECIMAL32, - loan, holidays, processor); - assertEquals(BigDecimal.valueOf(200.0).longValue(), amounts.getTotalOutstanding().getAmount().longValue()); - } - - @NotNull - private Loan prepareLoanWithInstallments(List rows) { - Loan loan = mock(Loan.class); - List installments = createInstallments(rows, loan, usd); - when(loan.getRepaymentScheduleInstallments()).thenReturn(installments); - when(loan.getCurrency()).thenReturn(usd); - return loan; - } - - private List createInstallments(List rows, Loan loan, MonetaryCurrency usd) { - AtomicInteger count = new AtomicInteger(1); - return rows.stream().map(row -> { - LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, count.incrementAndGet(), row.fromDate, - row.dueDate, row.principal, row.interest, row.fee, row.penalty, true, null, null, row.paid); - if (row.paid) { - installment.payPrincipalComponent(row.fromDate, Money.of(usd, row.principal)); - installment.payInterestComponent(row.fromDate, Money.of(usd, row.interest)); - installment.updateObligationMet(true); - } - return installment; - }).toList(); - } -}