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 { * * Note: The loan is interest-free 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 { *

*/ private LocalDate interestChargedFromDate; - private final Money inArrearsTolerance; + private Money inArrearsTolerance; - private final Integer graceOnArrearsAgeing; + private Integer graceOnArrearsAgeing; // added private LocalDate loanEndDate; - private final List disbursementDatas; + private List 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 periodNumbersApplicableForPrincipalGrace = new HashSet<>(); + private Set periodNumbersApplicableForPrincipalGrace = new HashSet<>(); - private final Set periodNumbersApplicableForInterestGrace = new HashSet<>(); + private Set 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 supportedInterestRefundTypes; + private LoanScheduleType loanScheduleType; + private LoanScheduleProcessingType loanScheduleProcessingType; + private boolean enableAccrualActivityPosting; + private List 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 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 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 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 separateTotalCompoundingPercentageCharges(final Set loanCharges) { Set 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 = 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 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 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()));