FINERACT-1981: Prepayment calculation
This commit is contained in:
+9
-8
@@ -59,7 +59,6 @@ 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.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
@@ -82,7 +81,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
|
||||
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.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
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;
|
||||
@@ -149,7 +148,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
// only for progressive loans
|
||||
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
|
||||
LocalDate disbursementDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
LocalDate disbursementDate, LocalDate currentDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
if (loanTransactions.isEmpty()) {
|
||||
@@ -213,7 +212,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
|
||||
createNewTransaction(oldTransaction, newTransaction, ctx);
|
||||
}
|
||||
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx);
|
||||
recalculateInterestForDate(currentDate, ctx);
|
||||
List<LoanTransaction> txs = changeOperations.stream() //
|
||||
.filter(ChangeOperation::isTransaction) //
|
||||
.map(e -> e.getLoanTransaction().get()).toList();
|
||||
@@ -248,7 +247,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
|
||||
LocalDate currentDate = DateUtils.getBusinessLocalDate();
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
|
||||
.getLeft();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -1590,11 +1591,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType,
|
||||
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) {
|
||||
PayableDetails payableDetails = emiCalculator.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate);
|
||||
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate);
|
||||
|
||||
switch (paymentAllocationType) {
|
||||
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getPayableInterest().getAmount());
|
||||
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getPayablePrincipal().getAmount());
|
||||
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount());
|
||||
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getDuePrincipal().getAmount());
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
public class OutstandingDetails {
|
||||
|
||||
private final Money outstandingPrincipal;
|
||||
private final Money outstandingInterest;
|
||||
}
|
||||
+3
-4
@@ -22,10 +22,9 @@ import lombok.Data;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
public class PayableDetails {
|
||||
public class PeriodDueDetails {
|
||||
|
||||
private final Money emi;
|
||||
private final Money payablePrincipal;
|
||||
private final Money payableInterest;
|
||||
private final Money outstandingBalance;
|
||||
private final Money duePrincipal;
|
||||
private final Money dueInterest;
|
||||
}
|
||||
+17
@@ -197,4 +197,21 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
return Money.zero(loanProductRelatedDetail.getCurrency(), mc);
|
||||
}
|
||||
|
||||
public Money getTotalDueInterest() {
|
||||
return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest))
|
||||
.reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Money getTotalDuePrincipal() {
|
||||
return repaymentPeriods.stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
|
||||
.reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Money getTotalPaidInterest() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidInterest).reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Money getTotalPaidPrincipal() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-9
@@ -46,7 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
@@ -195,7 +195,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
continue;
|
||||
}
|
||||
|
||||
Money outstandingBalance = emiCalculator.getOutstandingLoanBalance(interestScheduleModel, periodDueDate, disbursementDate);
|
||||
Money outstandingBalance = emiCalculator.getOutstandingLoanBalanceOfPeriod(interestScheduleModel, periodDueDate,
|
||||
disbursementDate);
|
||||
|
||||
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal(), mc);
|
||||
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
|
||||
@@ -263,17 +264,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
|
||||
};
|
||||
|
||||
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
|
||||
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate,
|
||||
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();
|
||||
|
||||
PayableDetails result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate);
|
||||
OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
|
||||
// TODO: We should add all the past due outstanding amounts as well
|
||||
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
|
||||
.principal(result.getOutstandingBalance()) //
|
||||
.interest(result.getPayableInterest());
|
||||
|
||||
installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate))
|
||||
.forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency)));
|
||||
.principal(result.getOutstandingPrincipal()) //
|
||||
.interest(result.getOutstandingInterest());//
|
||||
|
||||
installments.forEach(installment -> amounts //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
|
||||
+6
-3
@@ -25,7 +25,8 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
@@ -55,8 +56,10 @@ public interface EMICalculator {
|
||||
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
|
||||
Money principalAmount);
|
||||
|
||||
PayableDetails getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate payDate);
|
||||
PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate);
|
||||
|
||||
Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate);
|
||||
|
||||
OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate);
|
||||
}
|
||||
|
||||
+67
-21
@@ -35,11 +35,13 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -156,8 +158,63 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) {
|
||||
public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate,
|
||||
final LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
repaymentPeriodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
|
||||
boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||
.count() > 1L;
|
||||
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
|
||||
}
|
||||
|
||||
return new PeriodDueDetails(repaymentPeriod.getEmi(), //
|
||||
repaymentPeriod.getDuePrincipal(), //
|
||||
repaymentPeriod.getDueInterest()); //
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
repaymentPeriodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
|
||||
|
||||
return repaymentPeriod.getOutstandingLoanBalance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
|
||||
.reduce((one, two) -> two))
|
||||
.ifPresent(ip -> ip.setDueDate(targetDate)); //
|
||||
|
||||
calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy);
|
||||
scheduleModelCopy.repaymentPeriods()
|
||||
.forEach(rp -> rp.getInterestPeriods().stream().filter(ip -> targetDate.isBefore(ip.getDueDate())).forEach(ip -> {
|
||||
ip.setRateFactor(BigDecimal.ZERO);
|
||||
ip.setRateFactorTillPeriodDueDate(BigDecimal.ZERO);
|
||||
}));
|
||||
calculateOutstandingBalance(scheduleModelCopy);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy);
|
||||
|
||||
Money totalOutstandingPrincipal = MathUtil
|
||||
.negativeToZero(scheduleModelCopy.getTotalDuePrincipal().minus(scheduleModelCopy.getTotalPaidPrincipal()));
|
||||
Money totalOutstandingInterest = MathUtil
|
||||
.negativeToZero(scheduleModelCopy.getTotalDueInterest().minus(scheduleModelCopy.getTotalPaidInterest()));
|
||||
return new OutstandingDetails(totalOutstandingPrincipal, totalOutstandingInterest);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
LocalDate repaymentPeriodDueDate, LocalDate targetDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream()
|
||||
@@ -175,11 +232,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
|
||||
}
|
||||
scheduleModelCopy.repaymentPeriods().stream()
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()
|
||||
.ifPresent(rp -> rp.getInterestPeriods().stream()
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate()))
|
||||
.reduce((one, two) -> two).ifPresent(ip -> ip.setDueDate(targetDate)));
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) //
|
||||
.reduce((one, two) -> two))
|
||||
.ifPresent(ip -> ip.setDueDate(targetDate)); //
|
||||
interestPeriod.setDueDate(adjustedTargetDate);
|
||||
int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod);
|
||||
repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear();
|
||||
@@ -188,19 +246,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
calculateOutstandingBalance(scheduleModelCopy);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy);
|
||||
|
||||
boolean multiplePeriodIsUnpaid = scheduleModelCopy.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()).count() > 1L;
|
||||
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
|
||||
repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
|
||||
}
|
||||
|
||||
return new PayableDetails(repaymentPeriod.getEmi(), repaymentPeriod.getDuePrincipal(), repaymentPeriod.getDueInterest(),
|
||||
interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate) {
|
||||
return getPayableDetails(interestScheduleModel, repaymentPeriodDueDate, targetDate).getOutstandingBalance();
|
||||
return scheduleModelCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
-146
@@ -1,146 +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 org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
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.OutstandingAmountsDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
public class PrepaymentCalculationTest {
|
||||
|
||||
private static final MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
|
||||
private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
|
||||
private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
|
||||
.fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$"));
|
||||
|
||||
@Mock
|
||||
private LoanProductRelatedDetail loanProductRelatedDetail;
|
||||
|
||||
@Mock
|
||||
private LoanApplicationTerms loanApplicationTerms;
|
||||
|
||||
@Mock
|
||||
private AdvancedPaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor;
|
||||
|
||||
@Mock
|
||||
private EMICalculator emiCalculator;
|
||||
|
||||
@InjectMocks
|
||||
private ProgressiveLoanScheduleGenerator progressiveLoanScheduleGenerator;
|
||||
|
||||
private Loan loan;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
|
||||
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
|
||||
|
||||
loan = Mockito.mock(Loan.class);
|
||||
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||
when(loan.getDisbursementDate()).thenReturn(LocalDate.of(2022, 9, 7));
|
||||
when(loan.getRepaymentScheduleInstallments()).thenReturn(createRepaymentScheduleInstallments());
|
||||
|
||||
when(loanApplicationTerms.getPreClosureInterestCalculationStrategy())
|
||||
.thenReturn(LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE);
|
||||
|
||||
when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0));
|
||||
|
||||
List<RepaymentPeriod> repaymentPeriods = createMockRepaymentPeriods();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel = new ProgressiveLoanInterestScheduleModel(repaymentPeriods,
|
||||
loanProductRelatedDetail, 100, mc);
|
||||
|
||||
when(loanRepaymentScheduleTransactionProcessor.reprocessProgressiveLoanTransactions(Mockito.any(), Mockito.anyList(), Mockito.any(),
|
||||
Mockito.anyList(), Mockito.anySet())).thenReturn(org.apache.commons.lang3.tuple.Pair.of(null, scheduleModel));
|
||||
|
||||
PayableDetails payableDetails = new PayableDetails(Money.of(monetaryCurrency, BigDecimal.valueOf(200)),
|
||||
Money.of(monetaryCurrency, BigDecimal.valueOf(500)), Money.of(monetaryCurrency, BigDecimal.valueOf(0)),
|
||||
Money.of(monetaryCurrency, BigDecimal.valueOf(1000)));
|
||||
|
||||
when(emiCalculator.getPayableDetails(Mockito.any(ProgressiveLoanInterestScheduleModel.class), Mockito.any(LocalDate.class),
|
||||
Mockito.any(LocalDate.class))).thenReturn(payableDetails);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
moneyHelper.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculatePrepaymentAmount() {
|
||||
LocalDate prepaymentDate = LocalDate.of(2023, 6, 1);
|
||||
|
||||
OutstandingAmountsDTO result = progressiveLoanScheduleGenerator.calculatePrepaymentAmount(monetaryCurrency, prepaymentDate,
|
||||
loanApplicationTerms, mc, loan, null, loanRepaymentScheduleTransactionProcessor);
|
||||
|
||||
assertEquals("1000.00", result.principal().getAmount().toString());
|
||||
assertEquals("15.00", result.interest().getAmount().toString());
|
||||
}
|
||||
|
||||
private List<LoanRepaymentScheduleInstallment> createRepaymentScheduleInstallments() {
|
||||
LoanRepaymentScheduleInstallment installment1 = new LoanRepaymentScheduleInstallment(loan, 1, LocalDate.of(2022, 10, 1),
|
||||
LocalDate.of(2022, 11, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(10), BigDecimal.ZERO, BigDecimal.ZERO, false, null);
|
||||
|
||||
LoanRepaymentScheduleInstallment installment2 = new LoanRepaymentScheduleInstallment(loan, 2, LocalDate.of(2022, 11, 2),
|
||||
LocalDate.of(2022, 12, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(5), BigDecimal.ZERO, BigDecimal.ZERO, false, null);
|
||||
|
||||
return List.of(installment1, installment2);
|
||||
}
|
||||
|
||||
private List<RepaymentPeriod> createMockRepaymentPeriods() {
|
||||
RepaymentPeriod period1 = Mockito.mock(RepaymentPeriod.class);
|
||||
when(period1.getFromDate()).thenReturn(LocalDate.of(2022, 10, 1));
|
||||
when(period1.getDueDate()).thenReturn(LocalDate.of(2022, 11, 1));
|
||||
|
||||
RepaymentPeriod period2 = Mockito.mock(RepaymentPeriod.class);
|
||||
when(period2.getFromDate()).thenReturn(LocalDate.of(2022, 11, 2));
|
||||
when(period2.getDueDate()).thenReturn(LocalDate.of(2022, 12, 1));
|
||||
|
||||
return List.of(period1, period2);
|
||||
}
|
||||
}
|
||||
+43
-54
@@ -35,7 +35,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
@@ -558,17 +558,14 @@ class ProgressiveEMICalculatorTest {
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
// schedule 1st period 1st day
|
||||
PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1),
|
||||
LocalDate.of(2024, 1, 1));
|
||||
Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance()));
|
||||
Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest()));
|
||||
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1));
|
||||
Assertions.assertEquals(17.01, toDouble(dueAmounts.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.0, toDouble(dueAmounts.getDueInterest()));
|
||||
|
||||
// schedule 2nd period last day
|
||||
payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest()));
|
||||
dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(16.52, toDouble(dueAmounts.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(dueAmounts.getDueInterest()));
|
||||
|
||||
// pay off a period with balance correction
|
||||
final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1);
|
||||
@@ -576,11 +573,10 @@ class ProgressiveEMICalculatorTest {
|
||||
final Money op1stCorrectionAmount = toMoney(16.77);
|
||||
|
||||
// get remaining balance and dues for a date
|
||||
final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate,
|
||||
final PeriodDueDetails repaymentDetails1st = emiCalculator.getDueAmounts(interestSchedule, op1stCorrectionPeriodDueDate,
|
||||
op1stCorrectionDate);
|
||||
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest()));
|
||||
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest()));
|
||||
|
||||
emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount);
|
||||
emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, toMoney(0.24));
|
||||
@@ -599,11 +595,10 @@ class ProgressiveEMICalculatorTest {
|
||||
final Money op2ndCorrectionAmount = toMoney(16.42);
|
||||
|
||||
// get remaining balance and dues for a date
|
||||
final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate,
|
||||
final PeriodDueDetails repaymentDetails2st = emiCalculator.getDueAmounts(interestSchedule, op2ndCorrectionPeriodDueDate,
|
||||
op2ndCorrectionDate);
|
||||
Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest()));
|
||||
Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getDueInterest()));
|
||||
|
||||
emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount);
|
||||
emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, toMoney(0.49));
|
||||
@@ -619,18 +614,16 @@ class ProgressiveEMICalculatorTest {
|
||||
// check numbers on last period due date
|
||||
LocalDate periodDueDate = LocalDate.of(2024, 7, 1);
|
||||
LocalDate payDate = LocalDate.of(2024, 7, 1);
|
||||
final PayableDetails repaymentDetails3rd = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate);
|
||||
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest()));
|
||||
final PeriodDueDetails repaymentDetails3rd = emiCalculator.getDueAmounts(interestSchedule, periodDueDate, payDate);
|
||||
Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getDueInterest()));
|
||||
|
||||
// check numbers after the last period due date
|
||||
periodDueDate = LocalDate.of(2024, 7, 1);
|
||||
payDate = LocalDate.of(2024, 7, 15);
|
||||
final PayableDetails repaymentDetails4th = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate);
|
||||
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest()));
|
||||
final PeriodDueDetails repaymentDetails4th = emiCalculator.getDueAmounts(interestSchedule, periodDueDate, payDate);
|
||||
Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getDueInterest()));
|
||||
|
||||
// balance update on the last period, check the right interest interval split
|
||||
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 10), Money.of(monetaryCurrency, BigDecimal.ZERO));
|
||||
@@ -678,25 +671,22 @@ class ProgressiveEMICalculatorTest {
|
||||
final Money op1stCorrectionAmount = toMoney(15.0);
|
||||
|
||||
// get remaining balance and dues for a date
|
||||
final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate,
|
||||
final PeriodDueDetails repaymentDetails1st = emiCalculator.getDueAmounts(interestSchedule, op1stCorrectionPeriodDueDate,
|
||||
op1stCorrectionDate);
|
||||
Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest()));
|
||||
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest()));
|
||||
|
||||
PayableDetails details = null;
|
||||
// check getPayableDetails forcast
|
||||
details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest()));
|
||||
PeriodDueDetails details = null;
|
||||
// check getdueAmounts forcast
|
||||
details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(details.getDueInterest()));
|
||||
|
||||
// apply balance change and check again
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), op1stCorrectionDate, op1stCorrectionAmount);
|
||||
details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance()));
|
||||
Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(details.getPayableInterest()));
|
||||
details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(details.getDueInterest()));
|
||||
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(1.43));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(0.58));
|
||||
@@ -749,21 +739,20 @@ class ProgressiveEMICalculatorTest {
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
|
||||
// get remaining balance and dues on due date
|
||||
PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1),
|
||||
LocalDate.of(2024, 2, 1));
|
||||
Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest()));
|
||||
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1));
|
||||
Assertions.assertEquals(16.43, toDouble(dueAmounts.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.58, toDouble(dueAmounts.getDueInterest()));
|
||||
|
||||
// check numbers on payoff date
|
||||
payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15));
|
||||
Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest()));
|
||||
dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15));
|
||||
Assertions.assertEquals(16.75, toDouble(dueAmounts.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.26, toDouble(dueAmounts.getDueInterest()));
|
||||
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(16.75));
|
||||
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(0.26));
|
||||
|
||||
// check again numbers are zero
|
||||
// payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1),
|
||||
// dueAmounts = emiCalculator.getdueAmounts(interestSchedule, LocalDate.of(2024, 2, 1),
|
||||
// LocalDate.of(2024, 2, 1));
|
||||
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
|
||||
@@ -771,9 +760,9 @@ class ProgressiveEMICalculatorTest {
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
|
||||
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 1, 15), toMoney(17.01));
|
||||
|
||||
payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1));
|
||||
Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal()));
|
||||
Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest()));
|
||||
dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1));
|
||||
Assertions.assertEquals(15.21, toDouble(dueAmounts.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.5, toDouble(dueAmounts.getDueInterest()));
|
||||
|
||||
// check periods in model
|
||||
checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21);
|
||||
@@ -1261,9 +1250,9 @@ class ProgressiveEMICalculatorTest {
|
||||
private static void checkDailyInterest(final ProgressiveLoanInterestScheduleModel interestModel, final LocalDate repaymentPeriodDueDate,
|
||||
final LocalDate interestStartDay, final int dayOffset, final double dailyInterest, final double interest) {
|
||||
Money previousInterest = emiCalculator
|
||||
.getPayableDetails(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getPayableInterest();
|
||||
Money currentInterest = emiCalculator.getPayableDetails(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset))
|
||||
.getPayableInterest();
|
||||
.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getDueInterest();
|
||||
Money currentInterest = emiCalculator.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset))
|
||||
.getDueInterest();
|
||||
|
||||
Assertions.assertEquals(dailyInterest, toDouble(currentInterest.minus(previousInterest)));
|
||||
Assertions.assertEquals(interest, toDouble(currentInterest));
|
||||
|
||||
+4
-4
@@ -100,15 +100,15 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
|
||||
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
|
||||
|
||||
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
|
||||
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), transactionsToReprocess, loan.getCurrency(),
|
||||
installmentsToReprocess, loan.getActiveCharges());
|
||||
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
|
||||
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
|
||||
loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet());
|
||||
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
|
||||
|
||||
payableInterest = installmentsToReprocess.stream() //
|
||||
.map(installment -> emiCalculator //
|
||||
.getPayableDetails(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) //
|
||||
.getPayableInterest() //
|
||||
.getDueAmounts(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) //
|
||||
.getDueInterest() //
|
||||
.getAmount()) //
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add); //
|
||||
}
|
||||
|
||||
+191
@@ -5518,6 +5518,197 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
});
|
||||
}
|
||||
|
||||
// UC151: Validate Prepayment calculation till Pay-off date
|
||||
// 1. Create a Loan product with Adv. Pment. Alloc. and with Declining Balance, 7% interest rate
|
||||
// 2. Submit Loan, approve and Disburse
|
||||
// 3. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024
|
||||
// 4. Pay 1st installment fully on the due date of the 1st period
|
||||
// 5. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024
|
||||
// 6. Pay 2nd installment partially in the middle of 2nd period (15/02/2024)
|
||||
// 7. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024
|
||||
// 8. Prepay the loan on 15/02/2024
|
||||
@Test
|
||||
public void uc151() {
|
||||
String operationDate = "01 January 2024";
|
||||
AtomicLong createdLoanId = new AtomicLong();
|
||||
runAt(operationDate, () -> {
|
||||
Long clientId = client.getClientId();
|
||||
PostLoanProductsRequest product = create4IProgressive().interestRateFrequencyType(YEARS).numberOfRepayments(6)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(1L)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
.multiDisburseLoan(true)//
|
||||
.maxTrancheCount(10)//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.allowApprovedDisbursedAmountsOverApplied(null)//
|
||||
.overAppliedCalculationType(null)//
|
||||
.overAppliedNumber(null)//
|
||||
.installmentAmountInMultiplesOf(null)//
|
||||
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
|
||||
;//
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
|
||||
operationDate, 1000.0, 7.0, 6, null);
|
||||
|
||||
applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS)
|
||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
|
||||
|
||||
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
createdLoanId.set(loanResponse.getLoanId());
|
||||
|
||||
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
||||
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN)
|
||||
.approvedOnDate(operationDate).locale("en"));
|
||||
|
||||
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));
|
||||
|
||||
// Before 1st disbursement date
|
||||
HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2023, 12, 31));
|
||||
assertEquals(100.0f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
// On 1st day
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 1, 1));
|
||||
assertEquals(100.0f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
// On 2nd day
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 1, 2));
|
||||
assertEquals(100.02f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.02f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
// On due date of 1st period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 1));
|
||||
assertEquals(100.58f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.58f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
// On the 1st day of 2nd period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 2));
|
||||
assertEquals(100.60f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.60f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
// In the middle of 2nd period (15 Feb)
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 15));
|
||||
assertEquals(100.86f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.86f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
// On the due date of 2nd period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 3, 1));
|
||||
assertEquals(101.16f, prepayAmounts.get("amount"));
|
||||
assertEquals(1.16f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(100.0f, prepayAmounts.get("principalPortion"));
|
||||
});
|
||||
String repaymentDate = "01 February 2024";
|
||||
runAt(repaymentDate, () -> {
|
||||
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
|
||||
.transactionDate(repaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(17.01));
|
||||
// Before 1st disbursement date
|
||||
HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2023, 12, 31));
|
||||
assertEquals(83.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
// On 1st day
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 1, 1));
|
||||
assertEquals(83.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
// On 2nd day
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 1, 2));
|
||||
assertEquals(83.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.00f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
// On due date of 1st period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 1));
|
||||
assertEquals(83.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
// On the 1st day of 2nd period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 2));
|
||||
assertEquals(83.59f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.02f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
// In the middle of 2nd period (15 Feb)
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 15));
|
||||
assertEquals(83.81f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.24f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
// On the due date of 2nd period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 3, 1));
|
||||
assertEquals(84.06f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.49f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(83.57f, prepayAmounts.get("principalPortion"));
|
||||
});
|
||||
|
||||
String secondRepaymentDate = "15 February 2024";
|
||||
runAt(secondRepaymentDate, () -> {
|
||||
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
|
||||
.transactionDate(secondRepaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(5.0));
|
||||
// Before 1st disbursement date
|
||||
HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2023, 12, 31));
|
||||
assertEquals(78.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
// On 1st day
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 1, 1));
|
||||
assertEquals(78.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
// On 2nd day
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 1, 2));
|
||||
assertEquals(78.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.00f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
// On due date of 1st period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 1));
|
||||
assertEquals(78.57f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.0f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
// On the 1st day of 2nd period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 2));
|
||||
assertEquals(78.59f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.02f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
// In the middle of 2nd period (15 Feb)
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 2, 15));
|
||||
assertEquals(78.81f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.24f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
// On the due date of 2nd period
|
||||
prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(),
|
||||
LocalDate.of(2024, 3, 1));
|
||||
assertEquals(79.05f, prepayAmounts.get("amount"));
|
||||
assertEquals(0.48f, prepayAmounts.get("interestPortion"));
|
||||
assertEquals(78.57f, prepayAmounts.get("principalPortion"));
|
||||
|
||||
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
|
||||
.transactionDate(secondRepaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(78.81));
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
|
||||
assertTrue(loanDetails.getStatus().getClosedObligationsMet());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
|
||||
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
|
||||
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
|
||||
|
||||
+3
-3
@@ -867,8 +867,8 @@ public abstract class BaseLoanIntegrationTest {
|
||||
return postLoansRequest;
|
||||
}
|
||||
|
||||
protected PostLoansRequest applyPin4ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate,
|
||||
Double amount, Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||
protected PostLoansRequest applyLP2ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||
|
||||
PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId)
|
||||
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).productId(loanProductId)
|
||||
@@ -912,7 +912,7 @@ public abstract class BaseLoanIntegrationTest {
|
||||
|
||||
protected Long applyAndApproveProgressiveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyPin4ProgressiveLoanRequest(clientId, loanProductId,
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId,
|
||||
loanDisbursementDate, amount, interestRate, numberOfRepayments, customizer));
|
||||
|
||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public class LoanPayOffTest extends BaseLoanIntegrationTest {
|
||||
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
|
||||
applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
|
||||
applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
|
||||
loanId = postLoansResponse.getLoanId();
|
||||
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024"));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024");
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
|
||||
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(
|
||||
create4IProgressive().supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem(refundType));
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
|
||||
applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
|
||||
applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
|
||||
Long loanId = postLoansResponse.getLoanId();
|
||||
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024"));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 June 2024");
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public class LoanTransactionBackdatedProgressiveTest extends BaseLoanIntegration
|
||||
clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive());
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
|
||||
applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
|
||||
applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null));
|
||||
loanId = postLoansResponse.getLoanId();
|
||||
loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024"));
|
||||
disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024");
|
||||
|
||||
+9
@@ -1488,6 +1488,15 @@ public class LoanTransactionHelper extends IntegrationTest {
|
||||
return response;
|
||||
}
|
||||
|
||||
public HashMap getPrepayAmount(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanID,
|
||||
final LocalDate transactionDate) {
|
||||
final String URL = "/fineract-provider/api/v1/loans/" + loanID
|
||||
+ "/transactions/template?command=prepayLoan&locale=en&dateFormat=yyyy-MM-dd&transactionDate=" + transactionDate + "&"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
final HashMap response = Utils.performServerGet(requestSpec, responseSpec, URL, "");
|
||||
return response;
|
||||
}
|
||||
|
||||
private String createLoanRefundTransferURL() {
|
||||
return "/fineract-provider/api/v1/accounttransfers/refundByTransfer?tenantIdentifier=default";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user