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()));


Reply via email to