FINERACT-1981: separate library to generate loan schedule
This commit is contained in:
committed by
Adam Saghy
parent
b6ccb06159
commit
d4190ede90
+222
-52
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -60,34 +61,34 @@ import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
|
||||
@Slf4j
|
||||
public final class LoanApplicationTerms {
|
||||
|
||||
private final ApplicationCurrency currency;
|
||||
private ApplicationCurrency currency;
|
||||
|
||||
private final Calendar loanCalendar;
|
||||
private Calendar loanCalendar;
|
||||
private Integer loanTermFrequency;
|
||||
private final PeriodFrequencyType loanTermPeriodFrequencyType;
|
||||
private PeriodFrequencyType loanTermPeriodFrequencyType;
|
||||
private Integer numberOfRepayments;
|
||||
private Integer actualNumberOfRepayments;
|
||||
private final Integer repaymentEvery;
|
||||
private final PeriodFrequencyType repaymentPeriodFrequencyType;
|
||||
private Integer repaymentEvery;
|
||||
private PeriodFrequencyType repaymentPeriodFrequencyType;
|
||||
|
||||
private long variationDays = 0L;
|
||||
private final Integer fixedLength;
|
||||
private final Integer nthDay;
|
||||
private Integer fixedLength;
|
||||
private Integer nthDay;
|
||||
|
||||
private final DayOfWeekType weekDayType;
|
||||
private final AmortizationMethod amortizationMethod;
|
||||
private DayOfWeekType weekDayType;
|
||||
private AmortizationMethod amortizationMethod;
|
||||
|
||||
private final InterestMethod interestMethod;
|
||||
private InterestMethod interestMethod;
|
||||
private BigDecimal interestRatePerPeriod;
|
||||
private final PeriodFrequencyType interestRatePeriodFrequencyType;
|
||||
private PeriodFrequencyType interestRatePeriodFrequencyType;
|
||||
private BigDecimal annualNominalInterestRate;
|
||||
private final InterestCalculationPeriodMethod interestCalculationPeriodMethod;
|
||||
private final boolean allowPartialPeriodInterestCalcualtion;
|
||||
private InterestCalculationPeriodMethod interestCalculationPeriodMethod;
|
||||
private boolean allowPartialPeriodInterestCalcualtion;
|
||||
|
||||
private Money principal;
|
||||
private final LocalDate expectedDisbursementDate;
|
||||
private final LocalDate repaymentsStartingFromDate;
|
||||
private final LocalDate calculatedRepaymentsStartingFromDate;
|
||||
private LocalDate expectedDisbursementDate;
|
||||
private LocalDate repaymentsStartingFromDate;
|
||||
private LocalDate calculatedRepaymentsStartingFromDate;
|
||||
/**
|
||||
* Integer representing the number of 'repayment frequencies' or installments where 'grace' should apply to the
|
||||
* 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.
|
||||
*/
|
||||
private final Integer interestChargingGrace;
|
||||
private Integer interestChargingGrace;
|
||||
|
||||
/**
|
||||
* Legacy method of support 'grace' on the charging of interest on a loan.
|
||||
@@ -124,16 +125,16 @@ public final class LoanApplicationTerms {
|
||||
* </p>
|
||||
*/
|
||||
private LocalDate interestChargedFromDate;
|
||||
private final Money inArrearsTolerance;
|
||||
private Money inArrearsTolerance;
|
||||
|
||||
private final Integer graceOnArrearsAgeing;
|
||||
private Integer graceOnArrearsAgeing;
|
||||
|
||||
// added
|
||||
private LocalDate loanEndDate;
|
||||
|
||||
private final List<DisbursementData> disbursementDatas;
|
||||
private List<DisbursementData> disbursementDatas;
|
||||
|
||||
private final boolean multiDisburseLoan;
|
||||
private boolean multiDisburseLoan;
|
||||
|
||||
private BigDecimal fixedEmiAmount;
|
||||
|
||||
@@ -143,63 +144,63 @@ public final class LoanApplicationTerms {
|
||||
|
||||
private BigDecimal currentPeriodFixedPrincipalAmount;
|
||||
|
||||
private final BigDecimal actualFixedEmiAmount;
|
||||
private BigDecimal actualFixedEmiAmount;
|
||||
|
||||
private final BigDecimal maxOutstandingBalance;
|
||||
private BigDecimal maxOutstandingBalance;
|
||||
|
||||
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 final boolean allowCompoundingOnEod;
|
||||
private RecalculationFrequencyType compoundingFrequencyType;
|
||||
private boolean allowCompoundingOnEod;
|
||||
|
||||
private final BigDecimal principalThresholdForLastInstalment;
|
||||
private final Integer installmentAmountInMultiplesOf;
|
||||
private BigDecimal principalThresholdForLastInstalment;
|
||||
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 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 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
|
||||
private Integer excludePeriodsForCalculation = 0;
|
||||
@@ -212,7 +213,7 @@ public final class LoanApplicationTerms {
|
||||
private int extraPeriods = 0;
|
||||
private boolean isEqualAmortization;
|
||||
private Money interestTobeApproppriated;
|
||||
private final BigDecimal fixedPrincipalPercentagePerInstallment;
|
||||
private BigDecimal fixedPrincipalPercentagePerInstallment;
|
||||
|
||||
private LocalDate newScheduledDueDateStart;
|
||||
private boolean isDownPaymentEnabled;
|
||||
@@ -223,10 +224,179 @@ public final class LoanApplicationTerms {
|
||||
private RepaymentStartDateType repaymentStartDateType;
|
||||
private LocalDate submittedOnDate;
|
||||
private Money disbursedPrincipal;
|
||||
private final LoanScheduleType loanScheduleType;
|
||||
private final LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
private final boolean enableAccrualActivityPosting;
|
||||
private final List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes;
|
||||
private LoanScheduleType loanScheduleType;
|
||||
private LoanScheduleProcessingType loanScheduleProcessingType;
|
||||
private boolean enableAccrualActivityPosting;
|
||||
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,
|
||||
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
|
||||
|
||||
+34
@@ -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) {
|
||||
}
|
||||
+46
-26
@@ -69,13 +69,16 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
|
||||
// generate list of proposed schedule due dates
|
||||
LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO);
|
||||
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
|
||||
.fetchLoanTermDueDateVariationsData(loanEndDate);
|
||||
if (lastDueDateVariation != null) {
|
||||
loanEndDate = lastDueDateVariation.getDateValue();
|
||||
if (loanApplicationTerms.getLoanTermVariations() != null) {
|
||||
LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations()
|
||||
.fetchLoanTermDueDateVariationsData(loanEndDate);
|
||||
if (lastDueDateVariation != null) {
|
||||
loanEndDate = lastDueDateVariation.getDateValue();
|
||||
}
|
||||
}
|
||||
|
||||
// determine the total charges due at time of disbursement
|
||||
|
||||
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
|
||||
|
||||
// setup variables for tracking important facts required for loan
|
||||
@@ -113,12 +116,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
chargesDueAtTimeOfDisbursement);
|
||||
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
|
||||
|
||||
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
|
||||
final LocalDate interestRateChangeEffectiveDate = interestRateChange.getTermVariationApplicableFrom().minusDays(1);
|
||||
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
|
||||
if (interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getFromDate())
|
||||
&& !interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getDueDate())) {
|
||||
emiCalculator.changeInterestRate(interestScheduleModel, interestRateChangeEffectiveDate, newInterestRate);
|
||||
if (loanApplicationTerms.getLoanTermVariations() != null) {
|
||||
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
|
||||
final LocalDate interestRateChangeEffectiveDate = interestRateChange.getTermVariationApplicableFrom().minusDays(1);
|
||||
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
|
||||
if (interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getFromDate())
|
||||
&& !interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getDueDate())) {
|
||||
emiCalculator.changeInterestRate(interestScheduleModel, interestRateChangeEffectiveDate, newInterestRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +168,13 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
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) {
|
||||
if (loanApplicationTerms.getDisbursementDatas().isEmpty()) {
|
||||
loanApplicationTerms.getDisbursementDatas()
|
||||
@@ -282,9 +294,11 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
// Private, internal methods
|
||||
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
|
||||
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isDueAtDisbursement()) {
|
||||
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
|
||||
if (loanCharges != null) {
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isDueAtDisbursement()) {
|
||||
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
|
||||
}
|
||||
}
|
||||
}
|
||||
return chargesDueAtTimeOfDisbursement;
|
||||
@@ -312,10 +326,12 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
|
||||
final MathContext mc) {
|
||||
Money cumulative = Money.zero(monetaryCurrency);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
if (loanCharges != null) {
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
@@ -345,10 +361,12 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod,
|
||||
final MathContext mc) {
|
||||
Money cumulative = Money.zero(monetaryCurrency);
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
if (loanCharges != null) {
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed,
|
||||
totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cumulative;
|
||||
@@ -413,13 +431,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
|
||||
private Set<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
|
||||
Set<LoanCharge> interestCharges = new HashSet<>();
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
|
||||
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
|
||||
interestCharges.add(loanCharge);
|
||||
if (loanCharges != null) {
|
||||
for (final LoanCharge loanCharge : loanCharges) {
|
||||
if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest()
|
||||
|| loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) {
|
||||
interestCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
loanCharges.removeAll(interestCharges);
|
||||
}
|
||||
loanCharges.removeAll(interestCharges);
|
||||
return interestCharges;
|
||||
}
|
||||
|
||||
|
||||
+132
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
@@ -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.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -78,6 +79,11 @@ class ProgressiveEMICalculatorTest {
|
||||
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,
|
||||
final BigDecimal interestRate, LoanRepaymentScheduleInstallment period) {
|
||||
final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate()));
|
||||
|
||||
Reference in New Issue
Block a user