FINERACT-1968: Progressive Loan schedule handling - interest handling (FLAT interest, Same as repayment period, Equal installments)

This commit is contained in:
Soma Sörös
2025-07-09 14:39:18 +02:00
committed by Adam Saghy
parent 3f359bb368
commit 40f8b59ab4
21 changed files with 710 additions and 77 deletions
@@ -42,7 +42,7 @@ public class InterestScheduleModelServiceGsonContext {
public RepaymentPeriod createRepaymentPeriodInstance(Type type) {
if (type == RepaymentPeriod.class) {
setPrev(RepaymentPeriod.empty(getPrev(), getMc()));
setPrev(RepaymentPeriod.empty(getPrev(), getMc(), getLoanProductRelatedDetail()));
return getPrev();
}
throw new IllegalArgumentException("Unsupported RepaymentPeriod type: " + type);
@@ -35,6 +35,7 @@ import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
@@ -91,7 +92,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> {
RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc);
RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc, loanProductRelatedDetail);
prev.set(rp);
return rp;
}).toList();
@@ -114,10 +115,19 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
addDisbursement(scheduleModel, EmiChangeOperation.disburse(disbursementDueDate, disbursedAmount));
LocalDate effectiveDueDate = scheduleModel.loanProductRelatedDetail().getInterestCalculationPeriodMethod() != null
&& scheduleModel.loanProductRelatedDetail().getInterestCalculationPeriodMethod().isSameAsRepaymentPeriod()
&& !scheduleModel.loanProductRelatedDetail().isAllowPartialPeriodInterestCalculation()
? scheduleModel.repaymentPeriods().stream().filter(rp -> rp.getDueDate().isAfter(disbursementDueDate)).findFirst()
.map(RepaymentPeriod::getFromDate).orElse(disbursementDueDate)
: disbursementDueDate;
addDisbursement(scheduleModel, EmiChangeOperation.disburse(effectiveDueDate, disbursedAmount));
}
private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
scheduleModel.repaymentPeriods().stream().filter(rp -> !operation.getSubmittedOnDate().isAfter(rp.getFromDate())).forEach(
rp -> rp.setTotalDisbursedAmount(MathUtil.nullToZero(rp.getTotalDisbursedAmount()).add(operation.getAmount().getAmount())));
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(),
scheduleModel.zero(), scheduleModel.zero())
@@ -444,11 +454,33 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return scheduleModelCopy;
}
private void calculateEMIValueAndRateFactorsForFlatInterestMethod(final LocalDate calculateFromRepaymentPeriodDueDate,
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
if (relatedRepaymentPeriods.isEmpty()) {
return;
}
calculateEMIOnActualModelWithFlatInterestMethod(relatedRepaymentPeriods, scheduleModel);
}
/**
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
*/
private void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate,
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
switch (scheduleModel.loanProductRelatedDetail().getInterestMethod()) {
case FLAT ->
calculateEMIValueAndRateFactorsForFlatInterestMethod(calculateFromRepaymentPeriodDueDate, scheduleModel, operation);
case DECLINING_BALANCE -> calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(calculateFromRepaymentPeriodDueDate,
scheduleModel, operation);
default -> throw new UnsupportedOperationException(
"Unsupported interest method: " + scheduleModel.loanProductRelatedDetail().getInterestMethod());
}
}
private void calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(final LocalDate calculateFromRepaymentPeriodDueDate,
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopy();
@@ -637,6 +669,20 @@ public final class ProgressiveEMICalculator implements EMICalculator {
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0
&& (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy)
|| isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
final BigDecimal repaymentEvery = BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery());
if (loanProductRelatedDetail.getInterestCalculationPeriodMethod() != null
&& loanProductRelatedDetail.getInterestCalculationPeriodMethod().isSameAsRepaymentPeriod()) {
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isMonthly()) {
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(12), actualDaysInPeriod,
calculatedDaysInPeriod, mc);
}
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isWeekly()) {
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(52), actualDaysInPeriod,
calculatedDaysInPeriod, mc);
}
}
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
@@ -747,13 +793,26 @@ public final class ProgressiveEMICalculator implements EMICalculator {
repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate());
final BigDecimal actualDaysInPeriod = BigDecimal
.valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate));
final BigDecimal calculatedDaysInPeriod = BigDecimal
final BigDecimal calculatedDaysInRepaymentPeriod = BigDecimal
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear();
final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0
&& (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy)
|| isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()));
final BigDecimal daysInMonth = daysInMonthType.isDaysInMonth_30() ? BigDecimal.valueOf(30) : calculatedDaysInPeriod;
final BigDecimal daysInMonth = daysInMonthType.isDaysInMonth_30() ? BigDecimal.valueOf(30) : calculatedDaysInRepaymentPeriod;
if (loanProductRelatedDetail.getInterestCalculationPeriodMethod() != null
&& loanProductRelatedDetail.getInterestCalculationPeriodMethod().isSameAsRepaymentPeriod()) {
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isMonthly()) {
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(12), actualDaysInPeriod,
calculatedDaysInRepaymentPeriod, mc);
}
if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isWeekly()) {
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(52), actualDaysInPeriod,
calculatedDaysInRepaymentPeriod, mc);
}
}
// TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
@@ -769,7 +828,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
case ACTUAL -> rateFactorByRepaymentPeriod(interestRate, actualDaysInPeriod, BigDecimal.ONE, daysInYear, BigDecimal.ONE,
BigDecimal.ONE, mc);
case DAYS_30 -> calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery,
daysInMonth, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
daysInMonth, daysInYear, actualDaysInPeriod, calculatedDaysInRepaymentPeriod, mc);
default -> throw new UnsupportedOperationException("Unsupported combination: Days in month: " + daysInMonthType);
};
}
@@ -845,10 +904,51 @@ public final class ProgressiveEMICalculator implements EMICalculator {
};
}
private void calculateEMIOnActualModelWithFlatInterestMethod(List<RepaymentPeriod> repaymentPeriods,
ProgressiveLoanInterestScheduleModel scheduleModel) {
final MathContext mc = scheduleModel.mc();
final CurrencyData currency = scheduleModel.loanProductRelatedDetail().getCurrencyData();
RepaymentPeriod first = repaymentPeriods.getFirst();
RepaymentPeriod last = repaymentPeriods.getLast();
Money sumOfInterest = Money.zero(currency);
for (RepaymentPeriod rp : repaymentPeriods) {
Money interest = rp.calculateCalculatedDueInterest();
sumOfInterest = sumOfInterest.add(interest);
rp.setEmi(interest);
}
// already repaid principals should be subtracted from total disbursed amount to calculate correct EMI.
BigDecimal alreadyRepaidPrincipals = first.getPrevious()
.map(rp -> rp.calculateTotalDisbursedAmountTillGivenPeriod(null).subtract(rp.getOutstandingLoanBalance().getAmount()))
.orElse(BigDecimal.ZERO);
Money total = Money.of(currency, first.calculateTotalDisbursedAmountTillGivenPeriod(first.getLastInterestPeriod()))
.plus(sumOfInterest).minus(alreadyRepaidPrincipals);
Money periodEmi = total.dividedBy(repaymentPeriods.size(), mc);
Money remainder = total.minus(periodEmi.multipliedBy(repaymentPeriods.size(), mc));
repaymentPeriods.forEach(rp -> {
Money emi = rp.equals(last) ? periodEmi.add(remainder) : periodEmi;
rp.setEmi(emi);
rp.setOriginalEmi(emi);
rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance);
});
}
private void calculateEMIOnActualModel(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel) {
if (repaymentPeriods.isEmpty()) {
return;
}
switch (scheduleModel.loanProductRelatedDetail().getInterestMethod()) {
case FLAT -> calculateEMIOnActualModelWithFlatInterestMethod(repaymentPeriods, scheduleModel);
case DECLINING_BALANCE -> calculateEMIOnActualModelWithDecliningBalanceInterestMethod(repaymentPeriods, scheduleModel);
default -> throw new UnsupportedOperationException("Unsupported interest method");
}
}
private void calculateEMIOnActualModelWithDecliningBalanceInterestMethod(List<RepaymentPeriod> repaymentPeriods,
ProgressiveLoanInterestScheduleModel scheduleModel) {
final MathContext mc = scheduleModel.mc();
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
@@ -33,6 +33,7 @@ import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
@Getter
@ToString(exclude = { "repaymentPeriod" })
@@ -55,7 +56,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
/** Stores credited principals. Related transactions: Chargeback or Credit Balance Refound */
private Money creditedPrincipal;
/** Stores credited principals. Related transaction: Chargeback */
/** Stores credited interest. Related transaction: Chargeback */
private Money creditedInterest;
private Money disbursementAmount;
@@ -139,15 +140,26 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
}
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
? BigDecimal.ZERO //
: getOutstandingLoanBalance().getAmount() //
.multiply(getRateFactorTillPeriodDueDate(), mc) //
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
.multiply(BigDecimal.valueOf(getLength()), mc); //
final BigDecimal interestDueTillRepaymentDueDate = getCalculatedDueInterest(
getRepaymentPeriod().getLoanProductRelatedDetail().getInterestMethod(), lengthTillPeriodDueDate); //
return MathUtil.negativeToZero(MathUtil.add(mc, creditedInterest.getAmount(), interestDueTillRepaymentDueDate));
}
public BigDecimal getCalculatedDueInterest(InterestMethod method, long lengthTillPeriodDueDate) {
if (lengthTillPeriodDueDate == 0) {
return BigDecimal.ZERO;
}
BigDecimal baseAmount = switch (method) {
case FLAT -> MathUtil.nullToZero(getRepaymentPeriod().calculateTotalDisbursedAmountTillGivenPeriod(this));
case DECLINING_BALANCE -> getOutstandingLoanBalance().getAmount();
default -> throw new UnsupportedOperationException("Method not implemented: " + method);
};
return baseAmount //
.multiply(getRateFactorTillPeriodDueDate(), mc) //
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
.multiply(BigDecimal.valueOf(getLength()), mc);
}
public long getLength() {
return DateUtils.getDifferenceInDays(fromDate, dueDate);
}
@@ -199,18 +199,8 @@ public class ProgressiveLoanInterestScheduleModel {
if (balanceChangeDate == null) {
return Optional.empty();
}
// TODO use isInPeriod
return repaymentPeriods.stream()//
.filter(repaymentPeriod -> {
final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty();
if (isFirstPeriod) {
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
} else {
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
}
})//
.filter(period -> isInPeriod(balanceChangeDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
.findFirst();
}
@@ -34,6 +34,7 @@ import lombok.ToString;
import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.apache.fineract.portfolio.util.Memo;
@ToString(exclude = { "previous" })
@@ -79,8 +80,17 @@ public final class RepaymentPeriod {
@Setter
private boolean isInterestMoved = false;
@Setter
@Getter
private BigDecimal totalDisbursedAmount;
@JsonExclude
@Getter
private final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail;
private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc) {
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc,
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
this.previous = previous;
this.fromDate = fromDate;
this.dueDate = dueDate;
@@ -91,16 +101,19 @@ public final class RepaymentPeriod {
this.paidInterest = paidInterest;
this.futureUnrecognizedInterest = futureUnrecognizedInterest;
this.mc = mc;
this.loanProductRelatedDetail = loanProductRelatedDetail;
}
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc) {
return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc);
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc,
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc, loanProductRelatedDetail);
}
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) {
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc,
LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail) {
final Money zero = emi.zero();
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero,
zero, mc);
zero, mc, loanProductRelatedDetail);
// There is always at least 1 interest period, by default with same from-due date as repayment period
newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
return newRepaymentPeriod;
@@ -109,7 +122,8 @@ public final class RepaymentPeriod {
public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate,
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, repaymentPeriod.paidPrincipal,
repaymentPeriod.paidInterest, repaymentPeriod.futureUnrecognizedInterest, mc);
repaymentPeriod.paidInterest, repaymentPeriod.futureUnrecognizedInterest, mc,
repaymentPeriod.getLoanProductRelatedDetail());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
@@ -120,7 +134,8 @@ public final class RepaymentPeriod {
public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) {
final Money zero = repaymentPeriod.emi.zero();
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate,
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, zero, mc);
new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, zero, mc,
repaymentPeriod.getLoanProductRelatedDetail());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod);
@@ -160,13 +175,13 @@ public final class RepaymentPeriod {
@NotNull
public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) {
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest,
() -> new Object[] { this.previous, this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved });
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { this.previous,
this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved, this.totalDisbursedAmount });
}
return calculatedDueInterestCalculation.get();
}
private Money calculateCalculatedDueInterest() {
public Money calculateCalculatedDueInterest() {
Money calculatedDueInterest = getZero(mc);
if (!isInterestMoved) {
calculatedDueInterest = Money.of(emi.getCurrencyData(),
@@ -192,7 +207,8 @@ public final class RepaymentPeriod {
() -> MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest()
: MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
getPaidInterest(), false),
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest });
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount,
emi });
}
return dueInterestCalculation.get();
}
@@ -307,7 +323,7 @@ public final class RepaymentPeriod {
.minus(getDuePrincipal(), mc)//
.plus(getPaidPrincipal(), mc);//
return MathUtil.negativeToZero(calculatedOutStandingLoanBalance, mc);
}, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods });
}, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, totalDisbursedAmount });
}
return outstandingBalanceCalculation.get();
}
@@ -369,4 +385,22 @@ public final class RepaymentPeriod {
this.paidInterest = paidInterest.zero();
this.paidPrincipal = paidPrincipal.zero();
}
/**
* @param tillPeriod
* can be null. if null it calculates total disbursement including last interest period.
* @return disbursed amount til interest period.
*/
public BigDecimal calculateTotalDisbursedAmountTillGivenPeriod(InterestPeriod tillPeriod) {
BigDecimal res = MathUtil.nullToZero(getTotalDisbursedAmount());
for (InterestPeriod interestPeriod : this.interestPeriods) {
if (interestPeriod.equals(tillPeriod)) {
break;
}
if (!interestPeriod.getDueDate().equals(getFromDate()) && interestPeriod.getDisbursementAmount() != null) {
res = res.add(interestPeriod.getDisbursementAmount().getAmount());
}
}
return res;
}
}
@@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -63,7 +64,8 @@ class LoanScheduleGeneratorTest {
void testGenerateLoanSchedule() {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false, null);
NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false, null,
InterestMethod.DECLINING_BALANCE, true);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
@@ -101,7 +103,7 @@ class LoanScheduleGeneratorTest {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY,
DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null, false,
null);
null, InterestMethod.DECLINING_BALANCE, true);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
@@ -24,6 +24,8 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -42,6 +44,8 @@ import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductMinimumRepaymentScheduleRelatedDetail;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
@@ -56,6 +60,7 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.lang.NonNull;
@Slf4j
@ExtendWith(MockitoExtension.class)
class ProgressiveEMICalculatorTest {
@@ -99,6 +104,9 @@ class ProgressiveEMICalculatorTest {
public void setupTestDefaults() {
Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null);
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
}
private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType,
@@ -3226,6 +3234,404 @@ class ProgressiveEMICalculatorTest {
}
@Nested
public class InterestTypeFlat {
@BeforeEach
public void setupTestDefaults() {
Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false);
Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null);
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.FLAT);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
// InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
}
@Test
void test_sameAsRepayment_days_repay_every_6_periods_5() {
final BigDecimal interestRate = BigDecimal.valueOf(12.2);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(5);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(6);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 202.00, 2.00, 200.00, 800.00, false); //
checkPeriod(interestSchedule, 1, 202.00, 2.00, 200.00, 600.00, false); //
checkPeriod(interestSchedule, 2, 202.00, 2.00, 200.00, 400.00, false); //
checkPeriod(interestSchedule, 3, 202.00, 2.00, 200.00, 200.00, false); //
checkPeriod(interestSchedule, 4, 202.00, 2.00, 200.00, 0.00, false); //
}
@Test
void test_sameAsRepayment_week_repay_every_1_periods_10() {
final BigDecimal interestRate = BigDecimal.valueOf(5.2);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = expectedRepaymentWeeks(disbursementDate, 10, 1);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod())
.thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 101.00, 1.00, 100.00, 900.00, false); //
checkPeriod(interestSchedule, 1, 101.00, 1.00, 100.00, 800.00, false); //
checkPeriod(interestSchedule, 2, 101.00, 1.00, 100.00, 700.00, false); //
checkPeriod(interestSchedule, 3, 101.00, 1.00, 100.00, 600.00, false); //
checkPeriod(interestSchedule, 4, 101.00, 1.00, 100.00, 500.00, false); //
checkPeriod(interestSchedule, 5, 101.00, 1.00, 100.00, 400.00, false); //
checkPeriod(interestSchedule, 6, 101.00, 1.00, 100.00, 300.00, false); //
checkPeriod(interestSchedule, 7, 101.00, 1.00, 100.00, 200.00, false); //
checkPeriod(interestSchedule, 8, 101.00, 1.00, 100.00, 100.00, false); //
checkPeriod(interestSchedule, 9, 101.00, 1.00, 100.00, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_2_periods_8() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(8);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 145.00, 20.00, 125.00, 875.00, false); //
checkPeriod(interestSchedule, 1, 145.00, 20.00, 125.00, 750.00, false); //
checkPeriod(interestSchedule, 2, 145.00, 20.00, 125.00, 625.00, false); //
checkPeriod(interestSchedule, 3, 145.00, 20.00, 125.00, 500.00, false); //
checkPeriod(interestSchedule, 4, 145.00, 20.00, 125.00, 375.00, false); //
checkPeriod(interestSchedule, 5, 145.00, 20.00, 125.00, 250.00, false); //
checkPeriod(interestSchedule, 6, 145.00, 20.00, 125.00, 125.00, false); //
checkPeriod(interestSchedule, 7, 145.00, 20.00, 125.00, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_1_periods_3_second_disbursement_on_repayment_due_date_allow_partial() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusMonths(1), toMoney(250.0));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 916.67, false); //
checkPeriod(interestSchedule, 1, 470.84, 12.50, 458.34, 458.33, false); //
checkPeriod(interestSchedule, 2, 470.83, 12.50, 458.33, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_1_periods_3_second_disbursement_on_repayment_due_date_disallow_partial() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusMonths(1), toMoney(250.0));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 916.67, false); //
checkPeriod(interestSchedule, 1, 470.84, 12.50, 458.34, 458.33, false); //
checkPeriod(interestSchedule, 2, 470.83, 12.50, 458.33, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_1_periods_3() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(4), toMoney(250.0));
checkPeriod(interestSchedule, 0, 429.06, 12.18, 416.88, 833.12, false); //
checkPeriod(interestSchedule, 1, 429.06, 12.50, 416.56, 416.56, false); //
checkPeriod(interestSchedule, 2, 429.06, 12.50, 416.56, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(4), toMoney(250.0));
checkPeriod(interestSchedule, 0, 514.78, 14.35, 500.43, 999.57, false); //
checkPeriod(interestSchedule, 1, 514.78, 15.00, 499.78, 499.79, false); //
checkPeriod(interestSchedule, 2, 514.79, 15.00, 499.79, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_1_periods_20() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(20);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 60.00, 10.00, 50.00, 950.00, false); //
checkPeriod(interestSchedule, 1, 60.00, 10.00, 50.00, 900.00, false); //
checkPeriod(interestSchedule, 2, 60.00, 10.00, 50.00, 850.00, false); //
checkPeriod(interestSchedule, 3, 60.00, 10.00, 50.00, 800.00, false); //
checkPeriod(interestSchedule, 4, 60.00, 10.00, 50.00, 750.00, false); //
checkPeriod(interestSchedule, 5, 60.00, 10.00, 50.00, 700.00, false); //
checkPeriod(interestSchedule, 6, 60.00, 10.00, 50.00, 650.00, false); //
checkPeriod(interestSchedule, 7, 60.00, 10.00, 50.00, 600.00, false); //
checkPeriod(interestSchedule, 8, 60.00, 10.00, 50.00, 550.00, false); //
checkPeriod(interestSchedule, 9, 60.00, 10.00, 50.00, 500.00, false); //
checkPeriod(interestSchedule, 10, 60.00, 10.00, 50.00, 450.00, false); //
checkPeriod(interestSchedule, 11, 60.00, 10.00, 50.00, 400.00, false); //
checkPeriod(interestSchedule, 12, 60.00, 10.00, 50.00, 350.00, false); //
checkPeriod(interestSchedule, 13, 60.00, 10.00, 50.00, 300.00, false); //
checkPeriod(interestSchedule, 14, 60.00, 10.00, 50.00, 250.00, false); //
checkPeriod(interestSchedule, 15, 60.00, 10.00, 50.00, 200.00, false); //
checkPeriod(interestSchedule, 16, 60.00, 10.00, 50.00, 150.00, false); //
checkPeriod(interestSchedule, 17, 60.00, 10.00, 50.00, 100.00, false); //
checkPeriod(interestSchedule, 18, 60.00, 10.00, 50.00, 50.00, false); //
checkPeriod(interestSchedule, 19, 60.00, 10.00, 50.00, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_1_periods_24() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(24);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(7500.0));
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(2500.0));
checkPeriod(interestSchedule, 0, 516.67, 100.00, 416.67, 9583.33, false); //
checkPeriod(interestSchedule, 1, 516.67, 100.00, 416.67, 9166.66, false); //
checkPeriod(interestSchedule, 2, 516.67, 100.00, 416.67, 8749.99, false); //
checkPeriod(interestSchedule, 3, 516.67, 100.00, 416.67, 8333.32, false); //
checkPeriod(interestSchedule, 4, 516.67, 100.00, 416.67, 7916.65, false); //
checkPeriod(interestSchedule, 5, 516.67, 100.00, 416.67, 7499.98, false); //
checkPeriod(interestSchedule, 6, 516.67, 100.00, 416.67, 7083.31, false); //
checkPeriod(interestSchedule, 7, 516.67, 100.00, 416.67, 6666.64, false); //
checkPeriod(interestSchedule, 8, 516.67, 100.00, 416.67, 6249.97, false); //
checkPeriod(interestSchedule, 9, 516.67, 100.00, 416.67, 5833.30, false); //
checkPeriod(interestSchedule, 10, 516.67, 100.00, 416.67, 5416.63, false); //
checkPeriod(interestSchedule, 11, 516.67, 100.00, 416.67, 4999.96, false); //
checkPeriod(interestSchedule, 12, 516.67, 100.00, 416.67, 4583.29, false); //
checkPeriod(interestSchedule, 13, 516.67, 100.00, 416.67, 4166.62, false); //
checkPeriod(interestSchedule, 14, 516.67, 100.00, 416.67, 3749.95, false); //
checkPeriod(interestSchedule, 15, 516.67, 100.00, 416.67, 3333.28, false); //
checkPeriod(interestSchedule, 16, 516.67, 100.00, 416.67, 2916.61, false); //
checkPeriod(interestSchedule, 17, 516.67, 100.00, 416.67, 2499.94, false); //
checkPeriod(interestSchedule, 18, 516.67, 100.00, 416.67, 2083.27, false); //
checkPeriod(interestSchedule, 19, 516.67, 100.00, 416.67, 1666.60, false); //
checkPeriod(interestSchedule, 20, 516.67, 100.00, 416.67, 1249.93, false); //
checkPeriod(interestSchedule, 21, 516.67, 100.00, 416.67, 833.26, false); //
checkPeriod(interestSchedule, 22, 516.67, 100.00, 416.67, 416.59, false); //
checkPeriod(interestSchedule, 23, 516.59, 100.00, 416.59, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusMonths(3).plusDays(4), toMoney(1000.0));
checkPeriod(interestSchedule, 0, 516.67, 100.00, 416.67, 9583.33, false); //
checkPeriod(interestSchedule, 1, 516.67, 100.00, 416.67, 9166.66, false); //
checkPeriod(interestSchedule, 2, 516.67, 100.00, 416.67, 8749.99, false); //
checkPeriod(interestSchedule, 3, 574.22, 108.67, 465.55, 9284.44, false); //
checkPeriod(interestSchedule, 4, 574.22, 110.00, 464.22, 8820.22, false); //
checkPeriod(interestSchedule, 5, 574.22, 110.00, 464.22, 8356.00, false); //
checkPeriod(interestSchedule, 6, 574.22, 110.00, 464.22, 7891.78, false); //
checkPeriod(interestSchedule, 7, 574.22, 110.00, 464.22, 7427.56, false); //
checkPeriod(interestSchedule, 8, 574.22, 110.00, 464.22, 6963.34, false); //
checkPeriod(interestSchedule, 9, 574.22, 110.00, 464.22, 6499.12, false); //
checkPeriod(interestSchedule, 10, 574.22, 110.00, 464.22, 6034.90, false); //
checkPeriod(interestSchedule, 11, 574.22, 110.00, 464.22, 5570.68, false); //
checkPeriod(interestSchedule, 12, 574.22, 110.00, 464.22, 5106.46, false); //
checkPeriod(interestSchedule, 13, 574.22, 110.00, 464.22, 4642.24, false); //
checkPeriod(interestSchedule, 14, 574.22, 110.00, 464.22, 4178.02, false); //
checkPeriod(interestSchedule, 15, 574.22, 110.00, 464.22, 3713.80, false); //
checkPeriod(interestSchedule, 16, 574.22, 110.00, 464.22, 3249.58, false); //
checkPeriod(interestSchedule, 17, 574.22, 110.00, 464.22, 2785.36, false); //
checkPeriod(interestSchedule, 18, 574.22, 110.00, 464.22, 2321.14, false); //
checkPeriod(interestSchedule, 19, 574.22, 110.00, 464.22, 1856.92, false); //
checkPeriod(interestSchedule, 20, 574.22, 110.00, 464.22, 1392.70, false); //
checkPeriod(interestSchedule, 21, 574.22, 110.00, 464.22, 928.48, false); //
checkPeriod(interestSchedule, 22, 574.22, 110.00, 464.22, 464.26, false); //
checkPeriod(interestSchedule, 23, 574.26, 110.00, 464.26, 0.00, false); //
}
@Test
void test_sameAsRepayment_month_repay_every_1_periods_3__not_calculate_exact_days() {
final BigDecimal interestRate = BigDecimal.valueOf(12.0);
final LocalDate disbursementDate = LocalDate.of(2024, 1, 1);
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false);
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate);
final Integer installmentAmountInMultiplesOf = null;
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(1000.0));
checkPeriod(interestSchedule, 0, 343.33, 10.00, 333.33, 666.67, false); //
checkPeriod(interestSchedule, 1, 343.33, 10.00, 333.33, 333.34, false); //
checkPeriod(interestSchedule, 2, 343.34, 10.00, 333.34, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(4), toMoney(250.0));
checkPeriod(interestSchedule, 0, 429.17, 12.50, 416.67, 833.33, false); //
checkPeriod(interestSchedule, 1, 429.17, 12.50, 416.67, 416.66, false); //
checkPeriod(interestSchedule, 2, 429.16, 12.50, 416.66, 0.00, false); //
emiCalculator.addDisbursement(interestSchedule, disbursementDate.plusDays(17), toMoney(250.0));
checkPeriod(interestSchedule, 0, 515.00, 15.00, 500.00, 1000.00, false); //
checkPeriod(interestSchedule, 1, 515.00, 15.00, 500.00, 500.00, false); //
checkPeriod(interestSchedule, 2, 515.00, 15.00, 500.00, 0.00, false); //
}
}
// utilities
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
case MONTHS -> expectedRepaymentsMonthly(disbursementDate, loanProductRelatedDetail.getNumberOfRepayments(),
loanProductRelatedDetail.getRepayEvery());
case WEEKS -> expectedRepaymentWeeks(disbursementDate, loanProductRelatedDetail.getNumberOfRepayments(),
loanProductRelatedDetail.getRepayEvery());
case DAYS -> expectedRepaymentDays(disbursementDate, loanProductRelatedDetail.getNumberOfRepayments(),
loanProductRelatedDetail.getRepayEvery());
default -> throw new UnsupportedOperationException();
};
}
List<LoanScheduleModelRepaymentPeriod> expectedRepaymentDays(final LocalDate disbursementDate, final int periods, final int length) {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(periods);
IntStream.range(0, periods).forEach(i -> expectedRepaymentPeriods
.add(repayment(i + 1, disbursementDate.plusDays((long) i * length), disbursementDate.plusDays((long) (i + 1) * length))));
return expectedRepaymentPeriods;
}
List<LoanScheduleModelRepaymentPeriod> expectedRepaymentsMonthly(final LocalDate disbursementDate, final int periods,
final int length) {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(periods);
IntStream.range(0, periods).forEach(i -> expectedRepaymentPeriods.add(
repayment(i + 1, disbursementDate.plusMonths((long) i * length), disbursementDate.plusMonths((long) (i + 1) * length))));
return expectedRepaymentPeriods;
}
List<LoanScheduleModelRepaymentPeriod> expectedRepaymentWeeks(final LocalDate disbursementDate, final int periods, final int length) {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(periods);
IntStream.range(0, periods).forEach(i -> expectedRepaymentPeriods
.add(repayment(i + 1, disbursementDate.plusWeeks((long) i * length), disbursementDate.plusWeeks((long) (i + 1) * length))));
return expectedRepaymentPeriods;
}
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
final Money zeroAmount = Money.zero(currency);
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,