FINERACT-1981: separate library to generate loan schedule

This commit is contained in:
Andrii Kulminskyi
2024-10-16 11:37:09 +02:00
committed by Adam Saghy
parent b6ccb06159
commit d4190ede90
5 changed files with 440 additions and 78 deletions
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -60,34 +61,34 @@ import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
@Slf4j @Slf4j
public final class LoanApplicationTerms { public final class LoanApplicationTerms {
private final ApplicationCurrency currency; private ApplicationCurrency currency;
private final Calendar loanCalendar; private Calendar loanCalendar;
private Integer loanTermFrequency; private Integer loanTermFrequency;
private final PeriodFrequencyType loanTermPeriodFrequencyType; private PeriodFrequencyType loanTermPeriodFrequencyType;
private Integer numberOfRepayments; private Integer numberOfRepayments;
private Integer actualNumberOfRepayments; private Integer actualNumberOfRepayments;
private final Integer repaymentEvery; private Integer repaymentEvery;
private final PeriodFrequencyType repaymentPeriodFrequencyType; private PeriodFrequencyType repaymentPeriodFrequencyType;
private long variationDays = 0L; private long variationDays = 0L;
private final Integer fixedLength; private Integer fixedLength;
private final Integer nthDay; private Integer nthDay;
private final DayOfWeekType weekDayType; private DayOfWeekType weekDayType;
private final AmortizationMethod amortizationMethod; private AmortizationMethod amortizationMethod;
private final InterestMethod interestMethod; private InterestMethod interestMethod;
private BigDecimal interestRatePerPeriod; private BigDecimal interestRatePerPeriod;
private final PeriodFrequencyType interestRatePeriodFrequencyType; private PeriodFrequencyType interestRatePeriodFrequencyType;
private BigDecimal annualNominalInterestRate; private BigDecimal annualNominalInterestRate;
private final InterestCalculationPeriodMethod interestCalculationPeriodMethod; private InterestCalculationPeriodMethod interestCalculationPeriodMethod;
private final boolean allowPartialPeriodInterestCalcualtion; private boolean allowPartialPeriodInterestCalcualtion;
private Money principal; private Money principal;
private final LocalDate expectedDisbursementDate; private LocalDate expectedDisbursementDate;
private final LocalDate repaymentsStartingFromDate; private LocalDate repaymentsStartingFromDate;
private final LocalDate calculatedRepaymentsStartingFromDate; private LocalDate calculatedRepaymentsStartingFromDate;
/** /**
* Integer representing the number of 'repayment frequencies' or installments where 'grace' should apply to the * Integer representing the number of 'repayment frequencies' or installments where 'grace' should apply to the
* principal component of a loans repayment period (installment). * principal component of a loans repayment period (installment).
@@ -110,7 +111,7 @@ public final class LoanApplicationTerms {
* *
* <b>Note:</b> The loan is <i>interest-free</i> for the period of time indicated. * <b>Note:</b> The loan is <i>interest-free</i> for the period of time indicated.
*/ */
private final Integer interestChargingGrace; private Integer interestChargingGrace;
/** /**
* Legacy method of support 'grace' on the charging of interest on a loan. * Legacy method of support 'grace' on the charging of interest on a loan.
@@ -124,16 +125,16 @@ public final class LoanApplicationTerms {
* </p> * </p>
*/ */
private LocalDate interestChargedFromDate; private LocalDate interestChargedFromDate;
private final Money inArrearsTolerance; private Money inArrearsTolerance;
private final Integer graceOnArrearsAgeing; private Integer graceOnArrearsAgeing;
// added // added
private LocalDate loanEndDate; private LocalDate loanEndDate;
private final List<DisbursementData> disbursementDatas; private List<DisbursementData> disbursementDatas;
private final boolean multiDisburseLoan; private boolean multiDisburseLoan;
private BigDecimal fixedEmiAmount; private BigDecimal fixedEmiAmount;
@@ -143,63 +144,63 @@ public final class LoanApplicationTerms {
private BigDecimal currentPeriodFixedPrincipalAmount; private BigDecimal currentPeriodFixedPrincipalAmount;
private final BigDecimal actualFixedEmiAmount; private BigDecimal actualFixedEmiAmount;
private final BigDecimal maxOutstandingBalance; private BigDecimal maxOutstandingBalance;
private Money totalInterestDue; private Money totalInterestDue;
private final DaysInMonthType daysInMonthType; private DaysInMonthType daysInMonthType;
private final DaysInYearType daysInYearType; private DaysInYearType daysInYearType;
private final boolean interestRecalculationEnabled; private boolean interestRecalculationEnabled;
private final LoanRescheduleStrategyMethod rescheduleStrategyMethod; private LoanRescheduleStrategyMethod rescheduleStrategyMethod;
private final InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod; private InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod;
private final CalendarInstance restCalendarInstance; private CalendarInstance restCalendarInstance;
private final RecalculationFrequencyType recalculationFrequencyType; private RecalculationFrequencyType recalculationFrequencyType;
private final CalendarInstance compoundingCalendarInstance; private CalendarInstance compoundingCalendarInstance;
private final RecalculationFrequencyType compoundingFrequencyType; private RecalculationFrequencyType compoundingFrequencyType;
private final boolean allowCompoundingOnEod; private boolean allowCompoundingOnEod;
private final BigDecimal principalThresholdForLastInstalment; private BigDecimal principalThresholdForLastInstalment;
private final Integer installmentAmountInMultiplesOf; private Integer installmentAmountInMultiplesOf;
private final LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy; private LoanPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy;
private Money approvedPrincipal = null; private Money approvedPrincipal;
private final LoanTermVariationsDataWrapper variationsDataWrapper; private LoanTermVariationsDataWrapper variationsDataWrapper;
private Money adjustPrincipalForFlatLoans; private Money adjustPrincipalForFlatLoans;
private LocalDate seedDate; private LocalDate seedDate;
private final CalendarHistoryDataWrapper calendarHistoryDataWrapper; private CalendarHistoryDataWrapper calendarHistoryDataWrapper;
private final Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled; private Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled;
private final Integer numberOfDays; private Integer numberOfDays;
private final boolean isSkipRepaymentOnFirstDayOfMonth; private boolean isSkipRepaymentOnFirstDayOfMonth;
private final boolean isFirstRepaymentDateAllowedOnHoliday; private boolean isFirstRepaymentDateAllowedOnHoliday;
private final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI; private boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI;
private boolean isPrincipalCompoundingDisabledForOverdueLoans; private boolean isPrincipalCompoundingDisabledForOverdueLoans;
private final HolidayDetailDTO holidayDetailDTO; private HolidayDetailDTO holidayDetailDTO;
private final Set<Integer> periodNumbersApplicableForPrincipalGrace = new HashSet<>(); private Set<Integer> periodNumbersApplicableForPrincipalGrace = new HashSet<>();
private final Set<Integer> periodNumbersApplicableForInterestGrace = new HashSet<>(); private Set<Integer> periodNumbersApplicableForInterestGrace = new HashSet<>();
// used for FLAT loans when interest rate changed // used for FLAT loans when interest rate changed
private Integer excludePeriodsForCalculation = 0; private Integer excludePeriodsForCalculation = 0;
@@ -212,7 +213,7 @@ public final class LoanApplicationTerms {
private int extraPeriods = 0; private int extraPeriods = 0;
private boolean isEqualAmortization; private boolean isEqualAmortization;
private Money interestTobeApproppriated; private Money interestTobeApproppriated;
private final BigDecimal fixedPrincipalPercentagePerInstallment; private BigDecimal fixedPrincipalPercentagePerInstallment;
private LocalDate newScheduledDueDateStart; private LocalDate newScheduledDueDateStart;
private boolean isDownPaymentEnabled; private boolean isDownPaymentEnabled;
@@ -223,10 +224,179 @@ public final class LoanApplicationTerms {
private RepaymentStartDateType repaymentStartDateType; private RepaymentStartDateType repaymentStartDateType;
private LocalDate submittedOnDate; private LocalDate submittedOnDate;
private Money disbursedPrincipal; private Money disbursedPrincipal;
private final LoanScheduleType loanScheduleType; private LoanScheduleType loanScheduleType;
private final LoanScheduleProcessingType loanScheduleProcessingType; private LoanScheduleProcessingType loanScheduleProcessingType;
private final boolean enableAccrualActivityPosting; private boolean enableAccrualActivityPosting;
private final List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes; private List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes;
private LoanApplicationTerms(Builder builder) {
this.currency = builder.currency;
this.loanTermFrequency = builder.loanTermFrequency;
this.loanTermPeriodFrequencyType = builder.loanTermPeriodFrequencyType;
this.numberOfRepayments = builder.numberOfRepayments;
this.repaymentEvery = builder.repaymentEvery;
this.repaymentPeriodFrequencyType = builder.repaymentPeriodFrequencyType;
this.interestRatePerPeriod = builder.interestRatePerPeriod;
this.interestRatePeriodFrequencyType = builder.interestRatePeriodFrequencyType;
this.annualNominalInterestRate = builder.annualNominalInterestRate;
this.principal = builder.principal;
this.expectedDisbursementDate = builder.expectedDisbursementDate;
this.repaymentsStartingFromDate = builder.repaymentsStartingFromDate;
this.daysInMonthType = builder.daysInMonthType;
this.daysInYearType = builder.daysInYearType;
this.variationsDataWrapper = builder.variationsDataWrapper;
this.fixedLength = builder.fixedLength;
this.inArrearsTolerance = builder.inArrearsTolerance;
this.disbursementDatas = builder.disbursementDatas;
this.downPaymentAmount = builder.downPaymentAmount;
}
public static class Builder {
private ApplicationCurrency currency;
private Integer loanTermFrequency;
private PeriodFrequencyType loanTermPeriodFrequencyType;
private Integer numberOfRepayments;
private Integer repaymentEvery;
private PeriodFrequencyType repaymentPeriodFrequencyType;
private BigDecimal interestRatePerPeriod;
private PeriodFrequencyType interestRatePeriodFrequencyType;
private BigDecimal annualNominalInterestRate;
private Money principal;
private LocalDate expectedDisbursementDate;
private LocalDate repaymentsStartingFromDate;
private DaysInMonthType daysInMonthType;
private DaysInYearType daysInYearType;
private LoanTermVariationsDataWrapper variationsDataWrapper;
private Integer fixedLength;
private Money inArrearsTolerance;
private List<DisbursementData> disbursementDatas;
private Money downPaymentAmount;
public Builder currency(ApplicationCurrency currency) {
this.currency = currency;
return this;
}
public Builder loanTermFrequency(Integer loanTermFrequency) {
this.loanTermFrequency = loanTermFrequency;
return this;
}
public Builder loanTermPeriodFrequencyType(PeriodFrequencyType loanTermPeriodFrequencyType) {
this.loanTermPeriodFrequencyType = loanTermPeriodFrequencyType;
return this;
}
public Builder numberOfRepayments(Integer numberOfRepayments) {
this.numberOfRepayments = numberOfRepayments;
return this;
}
public Builder repaymentEvery(Integer repaymentEvery) {
this.repaymentEvery = repaymentEvery;
return this;
}
public Builder repaymentPeriodFrequencyType(PeriodFrequencyType repaymentPeriodFrequencyType) {
this.repaymentPeriodFrequencyType = repaymentPeriodFrequencyType;
return this;
}
public Builder interestRatePerPeriod(BigDecimal interestRatePerPeriod) {
this.interestRatePerPeriod = interestRatePerPeriod;
return this;
}
public Builder interestRatePeriodFrequencyType(PeriodFrequencyType interestRatePeriodFrequencyType) {
this.interestRatePeriodFrequencyType = interestRatePeriodFrequencyType;
return this;
}
public Builder annualNominalInterestRate(BigDecimal annualNominalInterestRate) {
this.annualNominalInterestRate = annualNominalInterestRate;
return this;
}
public Builder principal(Money principal) {
this.principal = principal;
return this;
}
public Builder expectedDisbursementDate(LocalDate expectedDisbursementDate) {
this.expectedDisbursementDate = expectedDisbursementDate;
return this;
}
public Builder repaymentsStartingFromDate(LocalDate repaymentsStartingFromDate) {
this.repaymentsStartingFromDate = repaymentsStartingFromDate;
return this;
}
public Builder daysInMonthType(DaysInMonthType daysInMonthType) {
this.daysInMonthType = daysInMonthType;
return this;
}
public Builder daysInYearType(DaysInYearType daysInYearType) {
this.daysInYearType = daysInYearType;
return this;
}
public Builder variationsDataWrapper(LoanTermVariationsDataWrapper variationsDataWrapper) {
this.variationsDataWrapper = variationsDataWrapper;
return this;
}
public Builder fixedLength(Integer fixedLength) {
this.fixedLength = fixedLength;
return this;
}
public Builder inArrearsTolerance(Money inArrearsTolerance) {
this.inArrearsTolerance = inArrearsTolerance;
return this;
}
public Builder disbursementDatas(List<DisbursementData> disbursementDatas) {
this.disbursementDatas = disbursementDatas;
return this;
}
public Builder downPaymentAmount(Money downPaymentAmount) {
this.downPaymentAmount = downPaymentAmount;
return this;
}
public LoanApplicationTerms build() {
return new LoanApplicationTerms(this);
}
}
public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData) {
Money principal = Money.of(modelData.currency().toData(), modelData.disbursementAmount());
Money downPaymentAmount = Money.zero(modelData.currency().toData());
if (modelData.downPaymentEnabled()) {
downPaymentAmount = Money.of(modelData.currency().toData(),
MathUtil.percentageOf(principal.getAmount(), modelData.disbursementAmount(), 19));
if (modelData.installmentAmountInMultiplesOf() != null) {
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, modelData.installmentAmountInMultiplesOf());
}
}
return new Builder().currency(modelData.currency()).loanTermFrequency(modelData.numberOfRepayments())
.loanTermPeriodFrequencyType(PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()))
.numberOfRepayments(modelData.numberOfRepayments()).repaymentEvery(modelData.repaymentFrequency())
.repaymentPeriodFrequencyType(PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()))
.interestRatePerPeriod(modelData.annualNominalInterestRate())
.interestRatePeriodFrequencyType(PeriodFrequencyType.valueOf(modelData.repaymentFrequencyType()))
.annualNominalInterestRate(modelData.annualNominalInterestRate()).principal(principal)
.expectedDisbursementDate(modelData.disbursementDate()).repaymentsStartingFromDate(modelData.scheduleGenerationStartDate())
.daysInMonthType(modelData.daysInMonth()).daysInYearType(modelData.daysInYear()).fixedLength(modelData.fixedLength())
.inArrearsTolerance(Money.zero(modelData.currency().toData())).disbursementDatas(new ArrayList<>())
.downPaymentAmount(downPaymentAmount).build();
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
@@ -0,0 +1,34 @@
/**
* 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 jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerationStartDate, @NotNull ApplicationCurrency currency,
@NotNull BigDecimal disbursementAmount, @NotNull LocalDate disbursementDate, @NotNull int numberOfRepayments,
@NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal annualNominalInterestRate,
@NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear,
BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength) {
}
@@ -69,13 +69,16 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
// generate list of proposed schedule due dates // generate list of proposed schedule due dates
LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations() if (loanApplicationTerms.getLoanTermVariations() != null) {
.fetchLoanTermDueDateVariationsData(loanEndDate); LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
if (lastDueDateVariation != null) { .fetchLoanTermDueDateVariationsData(loanEndDate);
loanEndDate = lastDueDateVariation.getDateValue(); if (lastDueDateVariation != null) {
loanEndDate = lastDueDateVariation.getDateValue();
}
} }
// determine the total charges due at time of disbursement // determine the total charges due at time of disbursement
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
// setup variables for tracking important facts required for loan // setup variables for tracking important facts required for loan
@@ -113,12 +116,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
chargesDueAtTimeOfDisbursement); chargesDueAtTimeOfDisbursement);
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber()); repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { if (loanApplicationTerms.getLoanTermVariations() != null) {
final LocalDate interestRateChangeEffectiveDate = interestRateChange.getTermVariationApplicableFrom().minusDays(1); for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); final LocalDate interestRateChangeEffectiveDate = interestRateChange.getTermVariationApplicableFrom().minusDays(1);
if (interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getFromDate()) final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
&& !interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getDueDate())) { if (interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getFromDate())
emiCalculator.changeInterestRate(interestScheduleModel, interestRateChangeEffectiveDate, newInterestRate); && !interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateChangeEffectiveDate, newInterestRate);
}
} }
} }
@@ -163,6 +168,13 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
} }
public LoanScheduleModel generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData);
return generate(mc, loanApplicationTerms, null, null);
}
private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) { private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) {
if (loanApplicationTerms.getDisbursementDatas().isEmpty()) { if (loanApplicationTerms.getDisbursementDatas().isEmpty()) {
loanApplicationTerms.getDisbursementDatas() loanApplicationTerms.getDisbursementDatas()
@@ -282,9 +294,11 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
// Private, internal methods // Private, internal methods
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) { private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
for (final LoanCharge loanCharge : loanCharges) { if (loanCharges != null) {
if (loanCharge.isDueAtDisbursement()) { for (final LoanCharge loanCharge : loanCharges) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); if (loanCharge.isDueAtDisbursement()) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
}
} }
} }
return chargesDueAtTimeOfDisbursement; return chargesDueAtTimeOfDisbursement;
@@ -312,10 +326,12 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
final MathContext mc) { final MathContext mc) {
Money cumulative = Money.zero(monetaryCurrency); Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) { if (loanCharges != null) {
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) { for (final LoanCharge loanCharge : loanCharges) {
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed, if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc); cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
}
} }
} }
return cumulative; return cumulative;
@@ -345,10 +361,12 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
final MathContext mc) { final MathContext mc) {
Money cumulative = Money.zero(monetaryCurrency); Money cumulative = Money.zero(monetaryCurrency);
for (final LoanCharge loanCharge : loanCharges) { if (loanCharges != null) {
if (loanCharge.isPenaltyCharge()) { for (final LoanCharge loanCharge : loanCharges) {
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed, if (loanCharge.isPenaltyCharge()) {
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc); cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
}
} }
} }
return cumulative; return cumulative;
@@ -413,13 +431,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
private Set<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) { private Set<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
Set<LoanCharge> interestCharges = new HashSet<>(); Set<LoanCharge> interestCharges = new HashSet<>();
for (final LoanCharge loanCharge : loanCharges) { if (loanCharges != null) {
if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest() for (final LoanCharge loanCharge : loanCharges) {
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) { if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
interestCharges.add(loanCharge); || loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
interestCharges.add(loanCharge);
}
} }
loanCharges.removeAll(interestCharges);
} }
loanCharges.removeAll(interestCharges);
return interestCharges; return interestCharges;
} }
@@ -0,0 +1,132 @@
/**
* 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.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
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.ArrayList;
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.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class LoanScheduleGeneratorTest {
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null);
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
private static final MonetaryCurrency MONETARY_CURRENCY = MonetaryCurrency.fromApplicationCurrency(APPLICATION_CURRENCY);
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);
private static final BigDecimal NOMINAL_INTEREST_RATE = BigDecimal.valueOf(9.99);
private static final int NUMBER_OF_REPAYMENTS = 6;
private static final int REPAYMENT_FREQUENCY = 1;
private static final String REPAYMENT_FREQUENCY_TYPE = "MONTHS";
private static final LocalDate DISBURSEMENT_DATE = LocalDate.of(2024, 1, 15);
@BeforeAll
public static void init() {
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
public static void destruct() {
moneyHelper.close();
}
@Test
void testGenerateLoanSchedule() {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), APPLICATION_CURRENCY,
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null);
final MathContext mc = MoneyHelper.getMathContext();
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
ScheduledDateGenerator mockScheduledDateGenerator = Mockito.mock(ScheduledDateGenerator.class);
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(mockScheduledDateGenerator, emiCalculator);
when(mockScheduledDateGenerator.generateRepaymentPeriods(any(), any(), any())).thenReturn(expectedRepaymentPeriods);
LoanScheduleModel loanSchedule = generator.generate(mc, modelData);
List<LoanScheduleModelPeriod> periods = loanSchedule.getPeriods();
assertEquals(7, periods.size(), "Expected 7 periods including the downpayment period.");
LoanScheduleModelDisbursementPeriod disbursementPeriod = (LoanScheduleModelDisbursementPeriod) periods.get(0);
assertNotNull(disbursementPeriod);
checkPeriod(periods.get(1), 1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(31.97),
BigDecimal.valueOf(0.88), BigDecimal.valueOf(32.85), BigDecimal.valueOf(160.25));
checkPeriod(periods.get(2), 2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(31.52),
BigDecimal.valueOf(1.33), BigDecimal.valueOf(32.85), BigDecimal.valueOf(128.73));
checkPeriod(periods.get(3), 3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), BigDecimal.valueOf(31.78),
BigDecimal.valueOf(1.07), BigDecimal.valueOf(32.85), BigDecimal.valueOf(96.95));
checkPeriod(periods.get(4), 4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), BigDecimal.valueOf(32.04),
BigDecimal.valueOf(0.81), BigDecimal.valueOf(32.85), BigDecimal.valueOf(64.91));
checkPeriod(periods.get(5), 5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), BigDecimal.valueOf(32.31),
BigDecimal.valueOf(0.54), BigDecimal.valueOf(32.85), BigDecimal.valueOf(32.60));
checkPeriod(periods.get(6), 6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), BigDecimal.valueOf(32.60),
BigDecimal.valueOf(0.27), BigDecimal.valueOf(32.87), BigDecimal.ZERO);
}
private void checkPeriod(LoanScheduleModelPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
LocalDate expectedDueDate, BigDecimal expectedPrincipalDue, BigDecimal expectedInterestDue, BigDecimal expectedTotalDue,
BigDecimal expectedOutstandingLoanBalance) {
LoanScheduleModelRepaymentPeriod repaymentPeriod = (LoanScheduleModelRepaymentPeriod) period;
assertEquals(expectedPeriodNumber, repaymentPeriod.getPeriodNumber());
assertEquals(expectedFromDate, repaymentPeriod.getFromDate());
assertEquals(expectedDueDate, repaymentPeriod.getDueDate());
assertEquals(0, expectedPrincipalDue.compareTo(repaymentPeriod.getPrincipalDue().getAmount()));
assertEquals(0, expectedInterestDue.compareTo(repaymentPeriod.getInterestDue().getAmount()));
assertEquals(0, expectedTotalDue.compareTo(repaymentPeriod.getTotalDue().getAmount()));
assertEquals(0, expectedOutstandingLoanBalance.compareTo(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
}
private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) {
final Money zeroAmount = Money.zero(MONETARY_CURRENCY);
return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount,
zeroAmount, zeroAmount, false);
}
}
@@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLo
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -78,6 +79,11 @@ class ProgressiveEMICalculatorTest {
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN)); moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
} }
@AfterAll
public static void destruct() {
moneyHelper.close();
}
private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType, private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType,
final BigDecimal interestRate, LoanRepaymentScheduleInstallment period) { final BigDecimal interestRate, LoanRepaymentScheduleInstallment period) {
final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()));