This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new d4190ede9 FINERACT-1981: separate library to generate loan schedule
d4190ede9 is described below
commit d4190ede90cad359b34719311846f4f4fec3f842
Author: Andrii Kulminskyi <[email protected]>
AuthorDate: Fri Sep 27 16:18:13 2024 +0300
FINERACT-1981: separate library to generate loan schedule
---
.../loanschedule/domain/LoanApplicationTerms.java | 274 +++++++++++++++++----
.../domain/LoanRepaymentScheduleModelData.java | 34 +++
.../domain/ProgressiveLoanScheduleGenerator.java | 72 ++++--
.../domain/LoanScheduleGeneratorTest.java | 132 ++++++++++
.../calc/ProgressiveEMICalculatorTest.java | 6 +
5 files changed, 440 insertions(+), 78 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
index dab51028d..8f13670bc 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java
@@ -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,
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
new file mode 100644
index 000000000..9695fe979
--- /dev/null
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java
@@ -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) {
+}
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
index ded468ca2..2824d91f7 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
@@ -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;
}
diff --git
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
new file mode 100644
index 000000000..7f8f3e1ae
--- /dev/null
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java
@@ -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);
+ }
+}
diff --git
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
index 463df79a9..a769e5b74 100644
---
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -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()));