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 db8000b4f FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly 
support plug to Repayment Schedule
db8000b4f is described below

commit db8000b4f3ebedfee00a888fe9fb0c099d1828a8
Author: Janos Meszaros <[email protected]>
AuthorDate: Fri Jun 21 17:21:06 2024 +0200

    FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly support plug to 
Repayment Schedule
---
 .../infrastructure/core/service/DateUtils.java     |  10 -
 .../infrastructure/core/service/MathUtil.java      |  10 +
 .../portfolio/common/domain/DaysInMonthType.java   |   8 +
 .../portfolio/common/domain/DaysInYearType.java    |   8 +
 .../domain/PreGeneratedLoanSchedulePeriod.java     |  96 ++++
 .../domain/ScheduledDateGenerator.java             |   4 +
 .../AbstractProgressiveLoanScheduleGenerator.java  |  15 +-
 .../domain/ProgressiveLoanScheduleGenerator.java   |  53 +-
 .../loanproduct/calc/EMICalculationResult.java     |  17 +-
 .../portfolio/loanproduct/calc/EMICalculator.java  | 132 +----
 .../loanproduct/calc/ProgressiveEMICalculator.java | 437 +++++++++++++++
 .../loanproduct/calc/EMICalculatorTest.java        | 159 ------
 .../calc/ProgressiveEMICalculatorTest.java         | 621 +++++++++++++++++++++
 .../domain/DefaultScheduledDateGenerator.java      |  28 +
 .../domain/DefaultScheduledDateGeneratorTest.java  |  42 ++
 15 files changed, 1312 insertions(+), 328 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
index e972204cd..bd235d4d2 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java
@@ -24,7 +24,6 @@ import jakarta.validation.constraints.NotNull;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
-import java.time.Year;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
@@ -35,7 +34,6 @@ import java.util.Locale;
 import org.apache.fineract.infrastructure.core.data.ApiParameterError;
 import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
 import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
-import org.apache.fineract.portfolio.common.domain.DaysInYearType;
 
 public final class DateUtils {
 
@@ -396,12 +394,4 @@ public final class DateUtils {
         return formatter;
     }
 
-    public static Integer daysInYear(final DaysInYearType daysInYear, final 
LocalDate referenceDate) {
-        return daysInYear.isActual() ? 
DateUtils.getDaysInYear(referenceDate.getYear()) : daysInYear.getValue();
-    }
-
-    public static Integer getDaysInYear(final Integer year) {
-        return Year.isLeap(year) ? 366 : 365;
-    }
-
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
index ba58b25f7..7bdd21e46 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java
@@ -467,4 +467,14 @@ public final class MathUtil {
         }
         return percentageOf;
     }
+
+    /**
+     * Remove unused zeros from end of BigDecimal fraction and keep the format 
"0.0"
+     *
+     * @param value
+     * @return
+     */
+    public static BigDecimal stripTrailingZeros(final BigDecimal value) {
+        return value == null ? null : new 
BigDecimal(value.stripTrailingZeros().toPlainString());
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
index 20eca6bfe..c7725d29b 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInMonthType.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.portfolio.common.domain;
 
+import java.time.LocalDate;
 import java.util.Arrays;
 
 /**
@@ -74,4 +75,11 @@ public enum DaysInMonthType {
     public boolean isDaysInMonth_30() {
         return DaysInMonthType.DAYS_30.getValue().equals(this.value);
     }
+
+    public Integer getNumberOfDays(final LocalDate referenceDate) {
+        if (referenceDate == null) {
+            return null;
+        }
+        return this == ACTUAL ? referenceDate.lengthOfMonth() : 
this.getValue();
+    }
 }
diff --git 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
index 80a42d484..dde64bf82 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearType.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.portfolio.common.domain;
 
+import java.time.LocalDate;
 import java.util.Arrays;
 
 /**
@@ -83,4 +84,11 @@ public enum DaysInYearType {
     public boolean isActual() {
         return DaysInYearType.ACTUAL.getValue().equals(this.value);
     }
+
+    public Integer getNumberOfDays(final LocalDate referenceDate) {
+        if (referenceDate == null) {
+            return null;
+        }
+        return this == ACTUAL ? referenceDate.lengthOfYear() : this.getValue();
+    }
 }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
new file mode 100644
index 000000000..636cf0046
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PreGeneratedLoanSchedulePeriod.java
@@ -0,0 +1,96 @@
+/**
+ * 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 java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.Set;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
+
+@Data
+@Accessors(fluent = true)
+public class PreGeneratedLoanSchedulePeriod implements LoanScheduleModelPeriod 
{
+
+    private Integer periodNumber;
+    private boolean isRepaymentPeriod;
+    private boolean isDownPaymentPeriod;
+    private LocalDate periodFromDate;
+    private LocalDate periodDueDate;
+    private BigDecimal principalDue;
+    private BigDecimal interestDue;
+    private BigDecimal feeChargesDue;
+    private BigDecimal penaltyChargesDue;
+    private BigDecimal rescheduleInterestPortion;
+    private boolean isRecalculatedInterestComponent;
+    private boolean isEMIFixedSpecificToInstallment;
+
+    public PreGeneratedLoanSchedulePeriod(Integer periodNumber, LocalDate 
periodFromDate, LocalDate periodDueDate) {
+        this.periodNumber = periodNumber;
+        this.isRepaymentPeriod = true;
+        this.isDownPaymentPeriod = false;
+        this.periodFromDate = periodFromDate;
+        this.periodDueDate = periodDueDate;
+        this.principalDue = BigDecimal.ZERO;
+        this.interestDue = BigDecimal.ZERO;
+        this.feeChargesDue = BigDecimal.ZERO;
+        this.penaltyChargesDue = BigDecimal.ZERO;
+        this.rescheduleInterestPortion = BigDecimal.ZERO;
+        this.isRecalculatedInterestComponent = false;
+        this.isEMIFixedSpecificToInstallment = false;
+    }
+
+    @Override
+    public LoanSchedulePeriodData toData() {
+        return null;
+    }
+
+    @Override
+    public void addLoanCharges(BigDecimal feeCharge, BigDecimal penaltyCharge) 
{
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addPrincipalAmount(Money principalDue) {
+        this.principalDue = this.principalDue.add(principalDue.getAmount());
+    }
+
+    @Override
+    public void addInterestAmount(Money interestDue) {
+        this.interestDue = this.principalDue.add(interestDue.getAmount());
+    }
+
+    @Override
+    public Set<LoanInterestRecalcualtionAdditionalDetails> 
getLoanCompoundingDetails() {
+        return Set.of();
+    }
+
+    @Override
+    public void setEMIFixedSpecificToInstallmentTrue() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setRescheduleInterestPortion(BigDecimal 
rescheduleInterestPortion) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
index 50edf46fa..1fd14d85f 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java
@@ -19,6 +19,7 @@
 package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
 
 import java.time.LocalDate;
+import java.util.List;
 import 
org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
 import org.apache.fineract.portfolio.calendar.domain.Calendar;
 import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@@ -26,6 +27,9 @@ import 
org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
 
 public interface ScheduledDateGenerator {
 
+    List<PreGeneratedLoanSchedulePeriod> 
generateRepaymentPeriods(LoanApplicationTerms loanApplicationTerms,
+            HolidayDetailDTO holidayDetailDTO);
+
     LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, 
HolidayDetailDTO holidayDetailDTO);
 
     LocalDate 
idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType 
repaymentPeriodFrequencyType, int repaidEvery,
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
index 4984ed514..960e79dd7 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java
@@ -48,6 +48,8 @@ import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleD
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculationResult;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
 import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
 
 public abstract class AbstractProgressiveLoanScheduleGenerator implements 
LoanScheduleGenerator {
@@ -83,6 +85,11 @@ public abstract class 
AbstractProgressiveLoanScheduleGenerator implements LoanSc
         List<LoanScheduleModelPeriod> periods = 
createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, 
scheduleParams,
                 chargesDueAtTimeOfDisbursement);
 
+        List<PreGeneratedLoanSchedulePeriod> expectedRepaymentPeriods = 
getScheduledDateGenerator()
+                .generateRepaymentPeriods(loanApplicationTerms, 
holidayDetailDTO);
+        EMICalculationResult emiCalculationResult = 
getEMICalculator().calculateEMIValueAndRateFactors(scheduleParams,
+                loanApplicationTerms.toLoanProductRelatedDetail(), 
expectedRepaymentPeriods, mc);
+
         boolean isFirstRepayment = true;
 
         // charges which depends on total loan interest will be added to this
@@ -115,10 +122,11 @@ public abstract class 
AbstractProgressiveLoanScheduleGenerator implements LoanSc
 
             // 5 determine principal,interest of repayment period
             PrincipalInterest principalInterestForThisPeriod = 
calculatePrincipalInterestComponentsForPeriod(loanApplicationTerms,
-                    scheduleParams, mc);
+                    scheduleParams, emiCalculationResult, mc);
 
             // update cumulative fields for principal
             
currentPeriodParams.setPrincipalForThisPeriod(principalInterestForThisPeriod.principal());
+            
currentPeriodParams.setInterestForThisPeriod(principalInterestForThisPeriod.interest());
             updateOutstandingBalance(scheduleParams, currentPeriodParams);
 
             if (scheduleParams.getOutstandingBalance().isLessThanZero() || 
!isNextRepaymentAvailable) {
@@ -226,7 +234,9 @@ public abstract class 
AbstractProgressiveLoanScheduleGenerator implements LoanSc
     public abstract PaymentPeriodsInOneYearCalculator 
getPaymentPeriodsInOneYearCalculator();
 
     public abstract PrincipalInterest 
calculatePrincipalInterestComponentsForPeriod(LoanApplicationTerms 
loanApplicationTerms,
-            LoanScheduleParams loanScheduleParams, MathContext mc);
+            LoanScheduleParams loanScheduleParams, EMICalculationResult 
emiCalculationResult, MathContext mc);
+
+    protected abstract EMICalculator getEMICalculator();
 
     // Private, internal methods
     private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final 
Set<LoanCharge> loanCharges) {
@@ -260,7 +270,6 @@ public abstract class 
AbstractProgressiveLoanScheduleGenerator implements LoanSc
                 loanScheduleParams.addOutstandingBalance(principalMoney);
                 
loanScheduleParams.addOutstandingBalanceAsPerRest(principalMoney);
                 loanScheduleParams.addPrincipalToBeScheduled(principalMoney);
-                
loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(principalMoney));
                 loanApplicationTerms.resetFixedEmiAmount();
                 if (loanApplicationTerms.isDownPaymentEnabled()) {
                     final LoanScheduleModelDownPaymentPeriod downPaymentPeriod 
= createDownPaymentPeriod(loanApplicationTerms,
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 f05675e3c..10bb88987 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
@@ -23,6 +23,8 @@ import java.math.MathContext;
 import lombok.RequiredArgsConstructor;
 import org.apache.fineract.organisation.monetary.domain.Money;
 import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculationResult;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -31,6 +33,7 @@ public class ProgressiveLoanScheduleGenerator extends 
AbstractProgressiveLoanSch
 
     private final ScheduledDateGenerator scheduledDateGenerator;
     private final PaymentPeriodsInOneYearCalculator 
paymentPeriodsInOneYearCalculator;
+    private final EMICalculator emiCalculator;
 
     @Override
     public ScheduledDateGenerator getScheduledDateGenerator() {
@@ -43,30 +46,32 @@ public class ProgressiveLoanScheduleGenerator extends 
AbstractProgressiveLoanSch
     }
 
     @Override
-    public PrincipalInterest 
calculatePrincipalInterestComponentsForPeriod(LoanApplicationTerms 
loanApplicationTerms,
-            LoanScheduleParams loanScheduleParams, MathContext mc) {
-        // TODO: handle interest calculation
-        int periodNumber = loanScheduleParams.getPeriodNumber();
-        BigDecimal fixedEMIAmount = loanApplicationTerms.getFixedEmiAmount();
-        Money calculatedPrincipal;
-        if (fixedEMIAmount == null) {
-            int noRemainingPeriod = 
loanApplicationTerms.getActualNoOfRepaymnets() - periodNumber + 1;
-            calculatedPrincipal = 
loanScheduleParams.getOutstandingBalance().dividedBy(noRemainingPeriod, 
mc.getRoundingMode());
-            if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != 
null) {
-                calculatedPrincipal = 
Money.roundToMultiplesOf(calculatedPrincipal,
-                        
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
-            }
-            
loanApplicationTerms.setFixedEmiAmount(calculatedPrincipal.getAmount());
-        } else {
-            calculatedPrincipal = Money.of(loanApplicationTerms.getCurrency(), 
fixedEMIAmount);
-        }
-        // adjust if needed
-        if (periodNumber == loanApplicationTerms.getActualNoOfRepaymnets()) {
-            Money remainingAmount = 
loanScheduleParams.getOutstandingBalance().minus(calculatedPrincipal);
-            calculatedPrincipal = calculatedPrincipal.plus(remainingAmount);
-        }
+    protected EMICalculator getEMICalculator() {
+        return emiCalculator;
+    }
+
+    @Override
+    public PrincipalInterest 
calculatePrincipalInterestComponentsForPeriod(final LoanApplicationTerms 
loanApplicationTerms,
+            final LoanScheduleParams loanScheduleParams, final 
EMICalculationResult emiCalculationResult, final MathContext mc) {
 
-        return new PrincipalInterest(calculatedPrincipal, 
Money.zero(loanApplicationTerms.getCurrency()),
-                Money.zero(loanApplicationTerms.getCurrency()));
+        final Money equalMonthlyInstallmentValue = 
Money.of(loanApplicationTerms.getCurrency(),
+                emiCalculationResult.getEqualMonthlyInstallmentValue());
+        final BigDecimal rateFactorMinus1 = 
emiCalculationResult.getNextRepaymentPeriodRateFactorMinus1();
+        final Money calculatedInterest = 
loanScheduleParams.getOutstandingBalance().multipliedBy(rateFactorMinus1);
+        final Money calculatedPrincipal = 
equalMonthlyInstallmentValue.minus(calculatedInterest);
+
+        return new PrincipalInterest(
+                
adjustCalculatedPrincipalWithRemainingBalanceInLastPeriod(calculatedPrincipal, 
loanApplicationTerms, loanScheduleParams),
+                calculatedInterest, 
Money.zero(loanApplicationTerms.getCurrency()));
+    }
+
+    private Money 
adjustCalculatedPrincipalWithRemainingBalanceInLastPeriod(final Money 
calculatedPrincipal,
+            final LoanApplicationTerms loanApplicationTerms, final 
LoanScheduleParams loanScheduleParams) {
+        final boolean isLastRepaymentPeriod = 
loanScheduleParams.getPeriodNumber() == 
loanApplicationTerms.getActualNoOfRepaymnets();
+        if (isLastRepaymentPeriod) {
+            final Money remainingAmount = 
loanScheduleParams.getOutstandingBalance().minus(calculatedPrincipal);
+            return calculatedPrincipal.plus(remainingAmount);
+        }
+        return calculatedPrincipal;
     }
 }
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
index 0fd12b5ee..71b9435c1 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java
@@ -21,20 +21,23 @@ package org.apache.fineract.portfolio.loanproduct.calc;
 import java.math.BigDecimal;
 import java.util.List;
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Getter;
+import lombok.RequiredArgsConstructor;
 
-@AllArgsConstructor(access = AccessLevel.PACKAGE)
+@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
 public class EMICalculationResult {
 
     @Getter
     private final BigDecimal equalMonthlyInstallmentValue;
     private final List<BigDecimal> repaymentPeriodRateFactorMinus1List;
 
-    public BigDecimal getRateFactorMinus1ForRepaymentPeriod(Integer 
repaymentPeriodNumber) {
-        return repaymentPeriodNumber != null && repaymentPeriodNumber > 0
-                && repaymentPeriodNumber <= 
repaymentPeriodRateFactorMinus1List.size()
-                        ? 
repaymentPeriodRateFactorMinus1List.get(repaymentPeriodNumber - 1)
-                        : BigDecimal.ZERO;
+    private int counter = 0;
+
+    public BigDecimal getNextRepaymentPeriodRateFactorMinus1() {
+        return counter < repaymentPeriodRateFactorMinus1List.size() ? 
repaymentPeriodRateFactorMinus1List.get(counter++) : BigDecimal.ZERO;
+    }
+
+    public void reset() {
+        counter = 0;
     }
 }
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
index 37659e62f..bf217b73e 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java
@@ -18,133 +18,15 @@
  */
 package org.apache.fineract.portfolio.loanproduct.calc;
 
-import java.math.BigDecimal;
 import java.math.MathContext;
 import java.util.List;
-import org.springframework.stereotype.Component;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
+import 
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
 
-@Component
-public class EMICalculator {
+public interface EMICalculator {
 
-    /**
-     * Calculate EMI parts and return an EMI calculation result object with 
repayment installment rate factors
-     *
-     * @param repaymentPeriodDays
-     *            List of day gaps between periods (zero interest period 
values should be 0)
-     * @param principal
-     * @param interestRate
-     * @param daysInYear
-     * @param mc
-     * @return
-     */
-    public EMICalculationResult calculateEMI(final List<Integer> 
repaymentPeriodDays, final BigDecimal principal,
-            final BigDecimal interestRate, final Integer daysInYear, final 
MathContext mc) {
-        final List<BigDecimal> rateFactorList = 
getRateFactorList(repaymentPeriodDays, interestRate, daysInYear, mc);
-        final BigDecimal rateFactorN = calculateRateFactorN(rateFactorList, 
mc);
-        final BigDecimal fnResult = calculateFnResult(rateFactorList, mc);
-
-        final BigDecimal emiValue = calculateEMIValue(rateFactorN, principal, 
fnResult, mc);
-        final List<BigDecimal> rateFactorMinus1List = 
getRateFactorMinus1List(rateFactorList, mc);
-
-        return new EMICalculationResult(emiValue, rateFactorMinus1List);
-    }
-
-    /**
-     * Calculate rate factors from repayment periods
-     *
-     * @param repaymentPeriodDays
-     * @param interestRate
-     * @param daysInYear
-     * @param mc
-     * @return
-     */
-    List<BigDecimal> getRateFactorList(final List<Integer> 
repaymentPeriodDays, final BigDecimal interestRate, final Integer daysInYear,
-            final MathContext mc) {
-        return repaymentPeriodDays.stream().map(daysInPeriod -> 
rateFactor(interestRate, daysInPeriod, daysInYear, mc)).toList();
-    }
-
-    /**
-     * Rate factor -1 values
-     *
-     * @param rateFactors
-     * @param mc
-     * @return
-     */
-    List<BigDecimal> getRateFactorMinus1List(final List<BigDecimal> 
rateFactors, final MathContext mc) {
-        return rateFactors.stream().map(it -> it.subtract(BigDecimal.ONE, 
mc)).toList();
-    }
-
-    /**
-     * Calculate Rate Factor Product from rate factors
-     *
-     * @param rateFactors
-     * @param mc
-     * @return
-     */
-    BigDecimal calculateRateFactorN(final List<BigDecimal> rateFactors, final 
MathContext mc) {
-        return rateFactors.stream().reduce(BigDecimal.ONE, (BigDecimal acc, 
BigDecimal value) -> acc.multiply(value, mc));
-    }
-
-    /**
-     * Summarize Fn values
-     *
-     * @param rateFactors
-     * @param mc
-     * @return
-     */
-    BigDecimal calculateFnResult(final List<BigDecimal> rateFactors, final 
MathContext mc) {
-        return rateFactors.stream().skip(1).reduce(BigDecimal.ONE,
-                (BigDecimal previousValue, BigDecimal rateFactor) -> 
fnValue(previousValue, rateFactor, mc));
-    }
-
-    /**
-     * Calculate the EMI (Equal Monthly Installment) value
-     *
-     * @param rateFactorN
-     * @param principal
-     * @param fnResult
-     * @param mc
-     * @return
-     */
-    BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final 
BigDecimal principal, final BigDecimal fnResult,
-            final MathContext mc) {
-        return rateFactorN.multiply(principal, mc).divide(fnResult, mc);
-    }
-
-    /**
-     * To calculate the monthly payment, we first need to calculate something 
called the Rate Factor. We're going to be
-     * using simple interest. The Rate Factor for simple interest is 
calculated by the following formula:
-     *
-     *
-     * R = 1 + (r * d / y)
-     *
-     * @param interestRate
-     *            (r)
-     * @param daysInPeriod
-     *            (d)
-     * @param daysInYear
-     *            (y)
-     */
-    BigDecimal rateFactor(final BigDecimal interestRate, final Integer 
daysInPeriod, final Integer daysInYear, final MathContext mc) {
-        final BigDecimal daysPeriod = BigDecimal.valueOf(daysInPeriod);
-        final BigDecimal daysYear = BigDecimal.valueOf(daysInYear);
-
-        return 
BigDecimal.ONE.add(interestRate.multiply(daysPeriod.divide(daysYear, mc), mc), 
mc);
-    }
-
-    /**
-     * To calculate the function value for each period, we are going to use 
the next formula:
-     *
-     * fn = 1 + fnValueFrom * rateFactorEnd
-     *
-     * @param previousFnValue
-     *
-     * @param currentRateFactor
-     *
-     * @param mc
-     *
-     */
-    BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal 
currentRateFactor, final MathContext mc) {
-        return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, 
mc), mc);
-    }
+    EMICalculationResult calculateEMIValueAndRateFactors(LoanScheduleParams 
scheduleParams,
+            LoanProductRelatedDetail loanProductRelatedDetail, List<? extends 
LoanScheduleModelPeriod> expectedRepaymentPeriods,
+            MathContext mc);
 }
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
new file mode 100644
index 000000000..477ff9377
--- /dev/null
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -0,0 +1,437 @@
+/**
+ * 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.loanproduct.calc;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.time.LocalDate;
+import java.time.Year;
+import java.util.List;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.MathUtil;
+import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
+import org.apache.fineract.portfolio.common.domain.DaysInYearType;
+import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
+import 
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.springframework.stereotype.Component;
+
+@Component
+public final class ProgressiveEMICalculator implements EMICalculator {
+
+    private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
+    private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
+
+    /**
+     * Calculate Equal Monthly Installment value and Rate Factor -1 values for 
calculate Interest
+     *
+     * @param scheduleParams
+     *            Loan Schedule Params
+     *
+     * @param loanProductRelatedDetail
+     *            Loan Product Related Detail from LoanTermApplication
+     *
+     * @param expectedRepaymentPeriods
+     *            Expected Repayment Periods
+     *
+     * @param mc
+     * @return
+     */
+    @Override
+    public EMICalculationResult calculateEMIValueAndRateFactors(final 
LoanScheduleParams scheduleParams,
+            final LoanProductRelatedDetail loanProductRelatedDetail, final 
List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
+            final MathContext mc) {
+        // TODO: decide which outstanding balance method needed in the future
+        final BigDecimal nominalInterestRatePerPeriod = 
calcNominalInterestRatePerPeriod(
+                loanProductRelatedDetail.getNominalInterestRatePerPeriod(), 
mc);
+        final BigDecimal outstandingBalance = 
scheduleParams.getOutstandingBalanceAsPerRest().getAmount().compareTo(BigDecimal.ZERO)
 > 0
+                ? scheduleParams.getOutstandingBalanceAsPerRest().getAmount()
+                : loanProductRelatedDetail.getPrincipal().getAmount();
+        final DaysInYearType daysInYearType = 
DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
+        final DaysInMonthType daysInMonthType = 
DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
+        final PeriodFrequencyType repaymentFrequency = 
loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
+        final BigDecimal repaymentEvery = 
BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery());
+
+        final List<BigDecimal> rateFactorList = 
getRateFactorList(expectedRepaymentPeriods, nominalInterestRatePerPeriod, 
daysInYearType,
+                daysInMonthType, repaymentFrequency, repaymentEvery, mc);
+
+        return calculateEMI(rateFactorList, outstandingBalance, mc);
+    }
+
+    /**
+     * Convert Interest Percentage to fraction of 1
+     *
+     * @param interestRate
+     *            Interest Rate in Percentage
+     *
+     * @param mc
+     *
+     * @return Rate Interest Rate in fraction format
+     */
+    BigDecimal calcNominalInterestRatePerPeriod(final BigDecimal interestRate, 
MathContext mc) {
+        return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, mc);
+    }
+
+    /**
+     * * Calculate rate factors from ONLY repayment periods
+     *
+     * @param expectedRepaymentPeriods
+     * @param interestRate
+     * @param daysInYearType
+     * @param daysInMonthType
+     * @param repaymentFrequency
+     * @param repaymentEvery
+     * @param mc
+     * @return
+     */
+    List<BigDecimal> getRateFactorList(final List<? extends 
LoanScheduleModelPeriod> expectedRepaymentPeriods,
+            final BigDecimal interestRate, final DaysInYearType 
daysInYearType, final DaysInMonthType daysInMonthType,
+            final PeriodFrequencyType repaymentFrequency, final BigDecimal 
repaymentEvery, final MathContext mc) {
+        return 
expectedRepaymentPeriods.stream().filter(LoanScheduleModelPeriod::isRepaymentPeriod)
+                .map(period -> 
MathUtil.stripTrailingZeros(calculateRateFactorPerPeriod(period, interestRate, 
daysInYearType,
+                        daysInMonthType, repaymentFrequency, repaymentEvery, 
mc)))
+                .toList();
+    }
+
+    /**
+     * Calculate Rate Factor for an exact Period
+     *
+     * @param repaymentPeriod
+     * @param interestRate
+     * @param daysInYearType
+     * @param daysInMonthType
+     * @param repaymentFrequency
+     * @param repaymentEvery
+     * @param mc
+     * @return
+     */
+    BigDecimal calculateRateFactorPerPeriod(final LoanScheduleModelPeriod 
repaymentPeriod, final BigDecimal interestRate,
+            final DaysInYearType daysInYearType, final DaysInMonthType 
daysInMonthType, final PeriodFrequencyType repaymentFrequency,
+            final BigDecimal repaymentEvery, final MathContext mc) {
+        BigDecimal daysInMonth = 
BigDecimal.valueOf(daysInMonthType.getNumberOfDays(repaymentPeriod.periodFromDate()));
+        BigDecimal daysInYear = 
BigDecimal.valueOf(daysInYearType.getNumberOfDays(repaymentPeriod.periodFromDate()));
+        final BigDecimal actualDaysInPeriod = BigDecimal
+                
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.periodFromDate(), 
repaymentPeriod.periodDueDate()));
+        final BigDecimal calculatedDaysInPeriod = BigDecimal
+                
.valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.periodFromDate(), 
repaymentPeriod.periodDueDate()));
+        final int numberOfYearsDifferenceInPeriod = 
repaymentPeriod.periodDueDate().getYear() - 
repaymentPeriod.periodFromDate().getYear();
+        final boolean partialPeriodCalculationNeeded = daysInYearType == 
DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0;
+
+        // TODO check: 
loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // 
calculate period data
+        // TODO review: (repayment frequency: days, weeks, years; validation 
day is month fix 30)
+        if (partialPeriodCalculationNeeded) {
+            final BigDecimal cumulatedPeriodFractions = 
calculatePeriodFractions(repaymentPeriod, mc);
+            return rateFactorByRepaymentPartialPeriod(interestRate, 
repaymentEvery, cumulatedPeriodFractions, BigDecimal.ONE,
+                    BigDecimal.ONE, mc);
+        }
+
+        return 
calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, 
repaymentFrequency, repaymentEvery, daysInMonth,
+                daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
+    }
+
+    /**
+     * Calculate Period fractions part based on how much year has in the period
+     *
+     * @param repaymentPeriod
+     * @param mc
+     * @return
+     */
+    BigDecimal calculatePeriodFractions(LoanScheduleModelPeriod 
repaymentPeriod, MathContext mc) {
+        BigDecimal cumulatedRateFactor = BigDecimal.ZERO;
+        int actualYear = repaymentPeriod.periodFromDate().getYear();
+        int endYear = repaymentPeriod.periodDueDate().getYear();
+        LocalDate actualDate = repaymentPeriod.periodFromDate();
+        LocalDate endOfActualYear;
+
+        while (actualYear <= endYear) {
+            endOfActualYear = actualYear == endYear ? 
repaymentPeriod.periodDueDate() : LocalDate.of(actualYear, 12, 31);
+            BigDecimal numberOfDaysInYear = 
BigDecimal.valueOf(Year.of(actualYear).length());
+            BigDecimal calculatedDaysInActualYear = 
BigDecimal.valueOf(DateUtils.getDifferenceInDays(actualDate, endOfActualYear));
+            cumulatedRateFactor = 
cumulatedRateFactor.add(calculatedDaysInActualYear.divide(numberOfDaysInYear, 
mc), mc);
+            actualDate = endOfActualYear;
+            actualYear++;
+        }
+        return cumulatedRateFactor;
+    }
+
+    /**
+     * Calculate Rate Factor based on Repayment Frequency Type
+     *
+     * @param interestRate
+     * @param repaymentFrequency
+     * @param repaymentEvery
+     * @param daysInMonth
+     * @param daysInYear
+     * @param actualDaysInPeriod
+     * @param calculatedDaysInPeriod
+     * @param mc
+     * @return
+     */
+    BigDecimal calculateRateFactorPerPeriodBasedOnRepaymentFrequency(final 
BigDecimal interestRate,
+            final PeriodFrequencyType repaymentFrequency, final BigDecimal 
repaymentEvery, final BigDecimal daysInMonth,
+            final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, 
final BigDecimal calculatedDaysInPeriod,
+            final MathContext mc) {
+        return switch (repaymentFrequency) {
+            case DAYS ->
+                rateFactorByRepaymentEveryDay(interestRate, repaymentEvery, 
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
+            case WEEKS ->
+                rateFactorByRepaymentEveryWeek(interestRate, repaymentEvery, 
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc);
+            case MONTHS -> rateFactorByRepaymentEveryMonth(interestRate, 
repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
+                    calculatedDaysInPeriod, mc);
+            default -> throw new UnsupportedOperationException("Invalid 
repayment frequency"); // not supported yet
+        };
+    }
+
+    /**
+     * Calculate EMI parts and return an EMI calculation result object with 
repayment installment rate factors
+     *
+     * @param rateFactorList
+     * @param principal
+     * @param mc
+     * @return
+     */
+    EMICalculationResult calculateEMI(final List<BigDecimal> rateFactorList, 
final BigDecimal principal, final MathContext mc) {
+        final BigDecimal rateFactorN = 
MathUtil.stripTrailingZeros(calculateRateFactorN(rateFactorList, mc));
+        final BigDecimal fnResult = 
MathUtil.stripTrailingZeros(calculateFnResult(rateFactorList, mc));
+
+        final BigDecimal emiValue = 
MathUtil.stripTrailingZeros(calculateEMIValue(rateFactorN, principal, fnResult, 
mc));
+        final List<BigDecimal> rateFactorMinus1List = 
getRateFactorMinus1List(rateFactorList, mc);
+
+        return new EMICalculationResult(emiValue, rateFactorMinus1List);
+    }
+
+    /**
+     * Rate factor -1 values
+     *
+     * @param rateFactors
+     * @param mc
+     * @return
+     */
+    List<BigDecimal> getRateFactorMinus1List(final List<BigDecimal> 
rateFactors, final MathContext mc) {
+        return rateFactors.stream().map(it -> it.subtract(BigDecimal.ONE, 
mc)).toList();
+    }
+
+    /**
+     * Calculate Rate Factor Product from rate factors
+     *
+     * @param rateFactors
+     * @param mc
+     * @return
+     */
+    BigDecimal calculateRateFactorN(final List<BigDecimal> rateFactors, final 
MathContext mc) {
+        return rateFactors.stream().reduce(BigDecimal.ONE, (BigDecimal acc, 
BigDecimal value) -> acc.multiply(value, mc));
+    }
+
+    /**
+     * Summarize Fn values
+     *
+     * @param rateFactors
+     * @param mc
+     * @return
+     */
+    BigDecimal calculateFnResult(final List<BigDecimal> rateFactors, final 
MathContext mc) {
+        return rateFactors.stream().skip(1).reduce(BigDecimal.ONE,
+                (BigDecimal previousValue, BigDecimal rateFactor) -> 
fnValue(previousValue, rateFactor, mc));
+    }
+
+    /**
+     * Calculate the EMI (Equal Monthly Installment) value
+     *
+     * @param rateFactorN
+     * @param principal
+     * @param fnResult
+     * @param mc
+     * @return
+     */
+    BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final 
BigDecimal principal, final BigDecimal fnResult,
+            final MathContext mc) {
+        return rateFactorN.multiply(principal, mc).divide(fnResult, mc);
+    }
+
+    /**
+     * To calculate the daily payment, we first need to calculate something 
called the Rate Factor. We're going to be
+     * using simple interest. The Rate Factor for simple interest is 
calculated by the following formula:
+     *
+     * Rate factor = 1 + (rate of interest * (repaid every / days in year) * 
actual days in period / calculated days in
+     * period ) Where
+     *
+     * @param interestRate
+     *            Rate of Interest
+     *
+     * @param repaymentEvery
+     *            Repaid Every
+     *
+     * @param daysInYear
+     *            Days is Year based on DaysInYear enum
+     *
+     * @param actualDaysInPeriod
+     *            Always the actual number of days in the actual period
+     *
+     * @param calculatedDaysInPeriod
+     *            Calculated days in Period (It has importance related to 
Reschedule)
+     *
+     * @param mc
+     * @return Rate Factor for period
+     */
+    BigDecimal rateFactorByRepaymentEveryDay(final BigDecimal interestRate, 
final BigDecimal repaymentEvery, final BigDecimal daysInYear,
+            final BigDecimal actualDaysInPeriod, final BigDecimal 
calculatedDaysInPeriod, final MathContext mc) {
+        return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, 
repaymentEvery, daysInYear, actualDaysInPeriod,
+                calculatedDaysInPeriod, mc);
+    }
+
+    /**
+     * To calculate the weekly payment, we first need to calculate something 
called the Rate Factor. We're going to be
+     * using simple interest. The Rate Factor for simple interest is 
calculated by the following formula:
+     *
+     * Rate factor = 1 + (rate of interest * (7 * repaid every / days in year) 
* actual days in period / calculated days
+     * in period ) Where
+     *
+     * @param interestRate
+     *            Rate of Interest
+     *
+     * @param repaymentEvery
+     *            Repaid Every
+     *
+     * @param daysInYear
+     *            Days is Year based on DaysInYear enum
+     *
+     * @param actualDaysInPeriod
+     *            Always the actual number of days in the actual period
+     *
+     * @param calculatedDaysInPeriod
+     *            Calculated days in Period (It has importance related to 
Reschedule)
+     *
+     * @param mc
+     * @return Rate Factor for period
+     */
+    BigDecimal rateFactorByRepaymentEveryWeek(final BigDecimal interestRate, 
final BigDecimal repaymentEvery, final BigDecimal daysInYear,
+            final BigDecimal actualDaysInPeriod, final BigDecimal 
calculatedDaysInPeriod, final MathContext mc) {
+        return rateFactorByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS, 
repaymentEvery, daysInYear, actualDaysInPeriod,
+                calculatedDaysInPeriod, mc);
+    }
+
+    /**
+     * To calculate the monthly payment, we first need to calculate something 
called the Rate Factor. We're going to be
+     * using simple interest. The Rate Factor for simple interest is 
calculated by the following formula:
+     *
+     * Rate factor = 1 + (rate of interest * (days in month * repaid every / 
days in year) * actual days in period /
+     * calculated days in period ) Where
+     *
+     * @param interestRate
+     *            Rate of Interest
+     *
+     * @param repaymentEvery
+     *            Repaid Every
+     *
+     * @param daysInMonth
+     *            Days in Month based on DaysInMonth enum
+     *
+     * @param daysInYear
+     *            Days is Year based on DaysInYear enum
+     *
+     * @param actualDaysInPeriod
+     *            Always the actual number of days in the actual period
+     *
+     * @param calculatedDaysInPeriod
+     *            Calculated days in Period (It has importance related to 
Reschedule)
+     *
+     * @param mc
+     * @return Rate Factor for period
+     */
+    BigDecimal rateFactorByRepaymentEveryMonth(final BigDecimal interestRate, 
final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
+            final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, 
final BigDecimal calculatedDaysInPeriod,
+            final MathContext mc) {
+        return rateFactorByRepaymentPeriod(interestRate, daysInMonth, 
repaymentEvery, daysInYear, actualDaysInPeriod,
+                calculatedDaysInPeriod, mc);
+    }
+
+    /**
+     * To calculate installment period payment. We're going to be using simple 
interest. The Rate Factor for simple
+     * interest is calculated by the following formula:
+     *
+     * Rate factor = 1 + (rate of interest * ( repayment period multiplier in 
days * repaid every * days in month / days
+     * in year) * actual days in period / calculated days in period ) Where
+     *
+     * @param interestRate
+     *            Rate of Interest
+     *
+     * @param repaymentPeriodMultiplierInDays
+     *            Multiplier number in days of the repayment every parameter
+     *
+     * @param repaymentEvery
+     *            Repaid Every
+     *
+     * @param daysInYear
+     *            Days is Year based on DaysInYear enum
+     *
+     * @param actualDaysInPeriod
+     *            Always the actual number of days in the actual period
+     *
+     * @param calculatedDaysInPeriod
+     *            Calculated days in Period (It has importance related to 
Reschedule)
+     *
+     * @param mc
+     * @return Rate Factor for period
+     */
+    BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, 
final BigDecimal repaymentPeriodMultiplierInDays,
+            final BigDecimal repaymentEvery, final BigDecimal daysInYear, 
final BigDecimal actualDaysInPeriod,
+            final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
+        final BigDecimal interestFractionPerPeriod = 
repaymentPeriodMultiplierInDays.multiply(repaymentEvery, mc).divide(daysInYear, 
mc);
+        return BigDecimal.ONE.add(
+                interestRate.multiply(interestFractionPerPeriod, 
mc).multiply(actualDaysInPeriod, mc).divide(calculatedDaysInPeriod, mc),
+                mc);
+    }
+
+    /**
+     * Calculate Rate Factor based on Partial Period
+     *
+     * @param interestRate
+     * @param repaymentEvery
+     * @param cumulatedPeriodRatio
+     * @param actualDaysInPeriod
+     * @param calculatedDaysInPeriod
+     * @param mc
+     * @return
+     */
+    BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal 
interestRate, final BigDecimal repaymentEvery,
+            final BigDecimal cumulatedPeriodRatio, final BigDecimal 
actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
+            final MathContext mc) {
+        final BigDecimal interestFractionPerPeriod = 
repaymentEvery.multiply(cumulatedPeriodRatio);
+        return BigDecimal.ONE.add(
+                interestRate.multiply(interestFractionPerPeriod, 
mc).multiply(actualDaysInPeriod, mc).divide(calculatedDaysInPeriod, mc),
+                mc);
+    }
+
+    /**
+     * To calculate the function value for each period, we are going to use 
the next formula:
+     *
+     * fn = 1 + fnValueFrom * rateFactorEnd
+     *
+     * @param previousFnValue
+     *
+     * @param currentRateFactor
+     *
+     * @param mc
+     *
+     */
+    BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal 
currentRateFactor, final MathContext mc) {
+        return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, 
mc), mc);
+    }
+}
diff --git 
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java
 
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java
deleted file mode 100644
index 30b819762..000000000
--- 
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculatorTest.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * 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.loanproduct.calc;
-
-import java.math.BigDecimal;
-import java.math.MathContext;
-import java.math.RoundingMode;
-import java.time.LocalDate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.apache.fineract.infrastructure.core.service.DateUtils;
-import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
-import org.apache.fineract.portfolio.common.domain.DaysInYearType;
-import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
-import org.jetbrains.annotations.NotNull;
-import org.junit.jupiter.api.Assertions;
-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 EMICalculatorTest {
-
-    private static EMICalculator emiCalculator = new EMICalculator();
-
-    private static MockedStatic<MoneyHelper> moneyHelper = 
Mockito.mockStatic(MoneyHelper.class);
-
-    private static List<LoanRepaymentScheduleInstallment> periods;
-    private final BigDecimal interestRate = BigDecimal.valueOf(0.094822);
-    private final BigDecimal principal = BigDecimal.valueOf(100);
-
-    @BeforeAll
-    public static void init() {
-        periods = new ArrayList<>();
-        LocalDate startDate = LocalDate.of(2024, 01, 1);
-        periods.add(createPeriod(1, startDate, startDate.plusMonths(1)));
-        periods.add(createPeriod(2, startDate.plusMonths(1), 
startDate.plusMonths(2)));
-        periods.add(createPeriod(3, startDate.plusMonths(2), 
startDate.plusMonths(3)));
-        periods.add(createPeriod(4, startDate.plusMonths(3), 
startDate.plusMonths(4)));
-        periods.add(createPeriod(5, startDate.plusMonths(4), 
startDate.plusMonths(5)));
-        periods.add(createPeriod(6, startDate.plusMonths(5), 
startDate.plusMonths(6)));
-
-        // When
-        moneyHelper.when(() -> 
MoneyHelper.getRoundingMode()).thenReturn(RoundingMode.HALF_EVEN);
-        moneyHelper.when(() -> MoneyHelper.getMathContext()).thenReturn(new 
MathContext(12, RoundingMode.HALF_EVEN));
-    }
-
-    @Test
-    public void testRateFactorFunctionDay365() {
-        // Given
-        final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
-        final String[] expectedValues = new String[] { "1.00805337534", 
"1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534",
-                "1.00779358904" };
-
-        // Then
-        for (LoanRepaymentScheduleInstallment period : periods) {
-            final Integer daysInPeriod = 
Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(), 
period.getDueDate()));
-            final Integer daysInYear = DateUtils.daysInYear(daysInYearType, 
period.getFromDate());
-            BigDecimal rateFactor = emiCalculator.rateFactor(interestRate, 
daysInPeriod, daysInYear, MoneyHelper.getMathContext());
-
-            
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], 
rateFactor.toString());
-        }
-    }
-
-    @Test
-    public void testRateFactorFunctionActual() {
-        // Given
-        final DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
-        final String[] expectedValues = new String[] { "1.00803137158", 
"1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158",
-                "1.00777229508" };
-
-        // Then
-        for (LoanRepaymentScheduleInstallment period : periods) {
-            final Integer daysInPeriod = 
Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(), 
period.getDueDate()));
-            final Integer daysInYear = DateUtils.daysInYear(daysInYearType, 
period.getFromDate());
-
-            BigDecimal rateFactor = emiCalculator.rateFactor(interestRate, 
daysInPeriod, daysInYear, MoneyHelper.getMathContext());
-
-            
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], 
rateFactor.toString());
-        }
-    }
-
-    @Test
-    public void testFnValueFunctionDay365() {
-        // Given
-        final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
-        final MathContext mc = MoneyHelper.getMathContext();
-        final String[] expectedValues = new String[] { "1.00000000000", 
"2.00753380274", "3.02370122596", "4.04726671069", "5.07986086861",
-                "6.11945121660" };
-
-        final List<BigDecimal> fnValuesCalculated = new ArrayList<>();
-        BigDecimal previousFnValue = BigDecimal.ZERO;
-        for (LoanRepaymentScheduleInstallment period : periods) {
-            final Integer daysInPeriod = 
Math.toIntExact(DateUtils.getDifferenceInDays(period.getFromDate(), 
period.getDueDate()));
-            final Integer daysInYear = DateUtils.daysInYear(daysInYearType, 
period.getFromDate());
-            final BigDecimal rateFactor = 
emiCalculator.rateFactor(interestRate, daysInPeriod, daysInYear, 
MoneyHelper.getMathContext());
-
-            final BigDecimal currentFnValue = 
emiCalculator.fnValue(previousFnValue, rateFactor, mc);
-            fnValuesCalculated.add(currentFnValue);
-
-            previousFnValue = currentFnValue;
-        }
-
-        int idx = 0;
-        for (BigDecimal fnValue : fnValuesCalculated) {
-            Assertions.assertEquals(expectedValues[idx++], fnValue.toString());
-        }
-    }
-
-    @Test
-    public void testEMICalculation() {
-        final MathContext mc = MoneyHelper.getMathContext();
-        final List<Integer> repaymentPeriodDays = Arrays.asList(31, 29, 31, 
30, 31, 30);
-
-        final EMICalculationResult result = 
emiCalculator.calculateEMI(repaymentPeriodDays, principal, interestRate,
-                DaysInYearType.DAYS_365.getValue(), mc);
-
-        // 17.13
-        Assertions.assertEquals(BigDecimal.valueOf(17.1293512777), 
result.getEqualMonthlyInstallmentValue());
-
-        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getRateFactorMinus1ForRepaymentPeriod(1));
-        Assertions.assertEquals(BigDecimal.valueOf(0.00753380274), 
result.getRateFactorMinus1ForRepaymentPeriod(2));
-        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getRateFactorMinus1ForRepaymentPeriod(3));
-        Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), 
result.getRateFactorMinus1ForRepaymentPeriod(4));
-        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getRateFactorMinus1ForRepaymentPeriod(5));
-        Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), 
result.getRateFactorMinus1ForRepaymentPeriod(6));
-    }
-
-    @NotNull
-    private static LoanRepaymentScheduleInstallment createPeriod(int periodId, 
LocalDate start, LocalDate end) {
-        LoanRepaymentScheduleInstallment period = 
Mockito.mock(LoanRepaymentScheduleInstallment.class);
-        Mockito.when(period.getInstallmentNumber()).thenReturn(periodId);
-        Mockito.when(period.getFromDate()).thenReturn(start);
-        Mockito.when(period.getDueDate()).thenReturn(end);
-
-        return period;
-    }
-
-}
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
new file mode 100644
index 000000000..447948e8b
--- /dev/null
+++ 
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -0,0 +1,621 @@
+/**
+ * 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.loanproduct.calc;
+
+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.infrastructure.core.service.DateUtils;
+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.common.domain.PeriodFrequencyType;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelDisbursementPeriod;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PreGeneratedLoanSchedulePeriod;
+import 
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Assertions;
+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 ProgressiveEMICalculatorTest {
+
+    private static final ProgressiveEMICalculator emiCalculator = new 
ProgressiveEMICalculator();
+
+    private static MockedStatic<MoneyHelper> moneyHelper = 
Mockito.mockStatic(MoneyHelper.class);
+    private static LoanScheduleParams scheduleParams = 
Mockito.mock(LoanScheduleParams.class);
+    private static LoanProductRelatedDetail loanProductRelatedDetail = 
Mockito.mock(LoanProductRelatedDetail.class);
+
+    private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
+            .fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2, 
1, "USD", "$"));
+
+    private static List<LoanRepaymentScheduleInstallment> periods;
+    private final BigDecimal interestRate = BigDecimal.valueOf(0.094822);
+
+    @BeforeAll
+    public static void init() {
+        periods = new ArrayList<>();
+        LocalDate startDate = LocalDate.of(2024, 01, 1);
+        periods.add(createPeriod(1, startDate, startDate.plusMonths(1)));
+        periods.add(createPeriod(2, startDate.plusMonths(1), 
startDate.plusMonths(2)));
+        periods.add(createPeriod(3, startDate.plusMonths(2), 
startDate.plusMonths(3)));
+        periods.add(createPeriod(4, startDate.plusMonths(3), 
startDate.plusMonths(4)));
+        periods.add(createPeriod(5, startDate.plusMonths(4), 
startDate.plusMonths(5)));
+        periods.add(createPeriod(6, startDate.plusMonths(5), 
startDate.plusMonths(6)));
+
+        // When
+        
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
+        moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new 
MathContext(12, RoundingMode.HALF_EVEN));
+    }
+
+    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()));
+        final BigDecimal daysInYear = 
BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate()));
+        final BigDecimal daysInMonth = 
BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate()));
+        return emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, 
BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod,
+                daysInPeriod, MoneyHelper.getMathContext());
+    }
+
+    @Test
+    public void 
testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() {
+        // Given
+        final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
+        final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+        final String[] expectedValues = new String[] { "1.00805337534", 
"1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534",
+                "1.00779358904" };
+
+        // Then
+        for (LoanRepaymentScheduleInstallment period : periods) {
+            BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, 
daysInMonthType, interestRate, period);
+            
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], 
rateFactor.toString());
+        }
+    }
+
+    @Test
+    public void 
testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() {
+        // Given
+        final DaysInYearType daysInYearType = DaysInYearType.ACTUAL;
+        final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+        final String[] expectedValues = new String[] { "1.00803137158", 
"1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158",
+                "1.00777229508" };
+
+        // Then
+        for (LoanRepaymentScheduleInstallment period : periods) {
+            BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, 
daysInMonthType, interestRate, period);
+            
Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], 
rateFactor.toString());
+        }
+    }
+
+    @Test
+    public void 
testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() {
+        // Given
+        final DaysInYearType daysInYearType = DaysInYearType.DAYS_365;
+        final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL;
+        final MathContext mc = MoneyHelper.getMathContext();
+        final String[] expectedValues = new String[] { "1.00000000000", 
"2.00753380274", "3.02370122596", "4.04726671069", "5.07986086861",
+                "6.11945121660" };
+
+        final List<BigDecimal> fnValuesCalculated = new ArrayList<>();
+        BigDecimal previousFnValue = BigDecimal.ZERO;
+        for (LoanRepaymentScheduleInstallment period : periods) {
+            BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, 
daysInMonthType, interestRate, period);
+
+            final BigDecimal currentFnValue = 
emiCalculator.fnValue(previousFnValue, rateFactor, mc);
+            fnValuesCalculated.add(currentFnValue);
+
+            previousFnValue = currentFnValue;
+        }
+
+        int idx = 0;
+        for (BigDecimal fnValue : fnValuesCalculated) {
+            Assertions.assertEquals(expectedValues[idx++], fnValue.toString());
+        }
+    }
+
+    @Test
+    public void 
testEMICalculation_principal100_dayInYearsActual_daysInMonthActual_repayEvery1Month()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 17.13
+        Assertions.assertEquals(BigDecimal.valueOf(17.1280789505), 
result.getEqualMonthlyInstallmentValue());
+
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00751321858), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00777229508), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00777229508), 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.valueOf(0), 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_principal10000_dayInYearsActual_daysInMonthActual_repayEvery1Month_DisbursementInPreviousYear()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 12)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 12)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 12)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 12)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(10_000);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 1713.12
+        Assertions.assertEquals(BigDecimal.valueOf(1713.12436983), 
result.getEqualMonthlyInstallmentValue());
+
+        Assertions.assertEquals(BigDecimal.valueOf(0.00804485776), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00751321858), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00777229508), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00803137158), 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_principal100_dayInYears365_daysInMonthActual_repayEvery1Month()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 17.13
+        Assertions.assertEquals(BigDecimal.valueOf(17.1293512777), 
result.getEqualMonthlyInstallmentValue());
+
+        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00753380274), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00779358904), 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(BigDecimal.valueOf(0.00805337534), 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_principal100_dayInYears360_daysInMonth30_repayEvery1Month() {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 17.13
+        Assertions.assertEquals(BigDecimal.valueOf(17.1306301273), 
result.getEqualMonthlyInstallmentValue());
+
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_principal100_dayInYears364_daysInMonthDoesntMatter_repayEvery1Week()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 1, 8), LocalDate.of(2024, 1, 15)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 22)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 1, 22), LocalDate.of(2024, 1, 29)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 1, 29), LocalDate.of(2024, 1, 5)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 16.77
+        Assertions.assertEquals(BigDecimal.valueOf(16.7731989919), 
result.getEqualMonthlyInstallmentValue());
+
+        final BigDecimal fixValue = new BigDecimal("0.0018235");
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_principal100_dayInYears360_daysInMonthDoesntMatter_repayEvery2Week()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 2, 12), LocalDate.of(2024, 2, 26)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 2, 26), LocalDate.of(2024, 3, 11)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 3, 11), LocalDate.of(2024, 3, 25)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 16.88
+        Assertions.assertEquals(new BigDecimal("16.8824319133"), 
result.getEqualMonthlyInstallmentValue());
+
+        final BigDecimal fixValue = new BigDecimal("0.00368752222");
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_principal100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 1, 16), LocalDate.of(2024, 1, 31)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 15)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 2, 15), LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31)));
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 16.90
+        Assertions.assertEquals(new BigDecimal("16.8978941103"), 
result.getEqualMonthlyInstallmentValue());
+
+        final BigDecimal fixValue = new BigDecimal("0.00395091667");
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(fixValue, 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void 
testEMICalculation_Disbursement_DownPayment_Principal100_dayInYears360_daysInMonth30_repayEvery1Month()
 {
+        final MathContext mc = MoneyHelper.getMathContext();
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final Money principal = Money.of(monetaryCurrency, 
BigDecimal.valueOf(100));
+        final Money downPaymentValue = Money.of(monetaryCurrency, 
BigDecimal.valueOf(25));
+        final Money outstandingBalance = principal.minus(downPaymentValue);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods
+                
.add(LoanScheduleModelDisbursementPeriod.disbursement(LocalDate.of(2024, 1, 1), 
principal, BigDecimal.ZERO));
+        expectedRepaymentPeriods
+                .add(LoanScheduleModelDownPaymentPeriod.downPayment(1, 
LocalDate.of(2024, 1, 1), downPaymentValue, outstandingBalance));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(5, 
LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(6, 
LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 17.13
+        Assertions.assertEquals(new BigDecimal("15.3574482569"), 
result.getEqualMonthlyInstallmentValue());
+
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(BigDecimal.valueOf(0.00790183333), 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void testEMICalculation_Principal1000_NoInterest_repayEvery1Month() 
{
+        final MathContext mc = MoneyHelper.getMathContext();
+
+        final BigDecimal interestRate = BigDecimal.valueOf(0);
+        final Money principal = Money.of(monetaryCurrency, 
BigDecimal.valueOf(1000));
+        final Money outstandingBalance = Money.of(monetaryCurrency, 
BigDecimal.ZERO);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getPrincipal()).thenReturn(principal);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(2, 
LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(3, 
LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(4, 
LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
+
+        final EMICalculationResult result = 
emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail,
+                expectedRepaymentPeriods, mc);
+
+        // 17.13
+        Assertions.assertEquals(new BigDecimal("250"), 
result.getEqualMonthlyInstallmentValue());
+
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        // no more period, no more interest
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+
+        result.reset();
+        // check reset
+        Assertions.assertEquals(BigDecimal.ZERO, 
result.getNextRepaymentPeriodRateFactorMinus1());
+    }
+
+    @Test
+    public void testUnsupportedRepaymentEveryYear() {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.YEARS);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+
+        try {
+            emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail, expectedRepaymentPeriods, mc);
+            Assertions.fail();
+        } catch (Exception e) {
+            Assertions.assertInstanceOf(UnsupportedOperationException.class, 
e);
+        }
+    }
+
+    @Test
+    public void testUnsupportedRepaymentEveryWholeTerm() {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WHOLE_TERM);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+
+        try {
+            emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail, expectedRepaymentPeriods, mc);
+            Assertions.fail();
+        } catch (Exception e) {
+            Assertions.assertInstanceOf(UnsupportedOperationException.class, 
e);
+        }
+    }
+
+    @Test
+    public void testInvalidRepaymentEveryValue() {
+        final MathContext mc = MoneyHelper.getMathContext();
+        final List<LoanScheduleModelPeriod> expectedRepaymentPeriods = new 
ArrayList<>();
+
+        final BigDecimal interestRate = BigDecimal.valueOf(9.4822);
+        final BigDecimal principal = BigDecimal.valueOf(100);
+        final Money outstandingBalance = Money.of(monetaryCurrency, principal);
+
+        
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
+        
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
+        
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
+        
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue());
+        
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.INVALID);
+        Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
+
+        expectedRepaymentPeriods.add(new PreGeneratedLoanSchedulePeriod(1, 
LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)));
+
+        try {
+            emiCalculator.calculateEMIValueAndRateFactors(scheduleParams, 
loanProductRelatedDetail, expectedRepaymentPeriods, mc);
+            Assertions.fail();
+        } catch (Exception e) {
+            Assertions.assertInstanceOf(UnsupportedOperationException.class, 
e);
+        }
+    }
+
+    @NotNull
+    private static LoanRepaymentScheduleInstallment createPeriod(int periodId, 
LocalDate start, LocalDate end) {
+        LoanRepaymentScheduleInstallment period = 
Mockito.mock(LoanRepaymentScheduleInstallment.class);
+        Mockito.when(period.getInstallmentNumber()).thenReturn(periodId);
+        Mockito.when(period.getFromDate()).thenReturn(start);
+        Mockito.when(period.getDueDate()).thenReturn(end);
+
+        return period;
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
index 7d3685d7b..1d2e9f487 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java
@@ -20,6 +20,8 @@ package 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
 
 import java.time.LocalDate;
 import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
 import lombok.extern.slf4j.Slf4j;
 import net.fortuna.ical4j.model.Recur;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -40,6 +42,32 @@ import org.springframework.stereotype.Component;
 @Slf4j
 public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
 
+    @Override
+    public List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(final 
LoanApplicationTerms loanApplicationTerms,
+            final HolidayDetailDTO holidayDetailDTO) {
+        final int numberOfRepayments = 
loanApplicationTerms.getNumberOfRepayments();
+        final ArrayList<PreGeneratedLoanSchedulePeriod> repaymentPeriods = new 
ArrayList<>(numberOfRepayments);
+        // TODO: check periodStartDate
+        LocalDate lastRepaymentDate = 
loanApplicationTerms.getRepaymentStartDate();
+        LocalDate nextRepaymentDate;
+        boolean isFirstRepayment = true;
+        for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; 
repaymentPeriod++) {
+            nextRepaymentDate = generateNextRepaymentDate(lastRepaymentDate, 
loanApplicationTerms, isFirstRepayment);
+
+            if (repaymentPeriod == numberOfRepayments) { // last period
+                if (loanApplicationTerms.getFixedLength() != null
+                        && 
loanApplicationTerms.calculateMaxDateForFixedLength().compareTo(nextRepaymentDate)
 != 0) {
+                    nextRepaymentDate = 
loanApplicationTerms.calculateMaxDateForFixedLength();
+                }
+                nextRepaymentDate = adjustRepaymentDate(nextRepaymentDate, 
loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
+            }
+            repaymentPeriods.add(new 
PreGeneratedLoanSchedulePeriod(repaymentPeriod, lastRepaymentDate, 
nextRepaymentDate));
+            lastRepaymentDate = nextRepaymentDate;
+            isFirstRepayment = false;
+        }
+        return repaymentPeriods;
+    }
+
     @Override
     public LocalDate getLastRepaymentDate(final LoanApplicationTerms 
loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) {
 
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
index 10d966e4d..5b6987bea 100644
--- 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java
@@ -38,6 +38,7 @@ import static org.mockito.BDDMockito.given;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.List;
 import 
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
 import org.apache.fineract.junit.context.WithTenantContext;
 import org.apache.fineract.junit.context.WithTenantContextExtension;
@@ -74,6 +75,47 @@ public class DefaultScheduledDateGeneratorTest {
         moneyHelper.initialize();
     }
 
+    @Test
+    @WithSystemTimeZone(EUROPE_BERLIN_ID)
+    @WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)
+    @WithSystemProperty(key = FLOATING_TIMEZONE_PROPERTY_KEY, value = "true")
+    public void test_generateRepaymentPeriods() {
+        // given
+        HolidayDetailDTO holidayDetailDTO = createHolidayDTO();
+
+        ApplicationCurrency dollarCurrency = new ApplicationCurrency("USD", 
"US Dollar", 2, 0, "currency.USD", "$");
+        Money principalAmount = 
Money.of(fromApplicationCurrency(dollarCurrency), BigDecimal.valueOf(100));
+        LocalDate expectedDisbursementDate = LocalDate.of(2024, 1, 1);
+        LocalDate dueRepaymentPeriodDate = LocalDate.of(2024, 2, 1);
+
+        LocalDate submittedOnDate = LocalDate.of(2024, 1, 1);
+        LoanApplicationTerms loanApplicationTerms = 
LoanApplicationTerms.assembleFrom(dollarCurrency, 1, MONTHS, 4, 1, MONTHS, null,
+                INVALID, EQUAL_PRINCIPAL, FLAT, ZERO, MONTHS, ZERO, 
SAME_AS_REPAYMENT_PERIOD, false, principalAmount,
+                expectedDisbursementDate, null, dueRepaymentPeriodDate, null, 
null, null, null, null,
+                Money.of(fromApplicationCurrency(dollarCurrency), ZERO), 
false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null,
+                DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, 
null, null, null, null, ZERO, null, NONE, null, ZERO,
+                EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, 
false, null, false, false, null, false, DISBURSEMENT_DATE,
+                submittedOnDate, CUMULATIVE, 
LoanScheduleProcessingType.HORIZONTAL, null);
+
+        // when
+        List<PreGeneratedLoanSchedulePeriod> result = 
underTest.generateRepaymentPeriods(loanApplicationTerms, holidayDetailDTO);
+
+        // then
+        assertThat(result.size()).isEqualTo(4);
+        assertThat(result.get(0).periodNumber()).isEqualTo(1);
+        
assertThat(result.get(0).periodFromDate().toString()).isEqualTo("2024-01-01");
+        
assertThat(result.get(0).periodDueDate().toString()).isEqualTo("2024-02-01");
+        assertThat(result.get(1).periodNumber()).isEqualTo(2);
+        
assertThat(result.get(1).periodFromDate().toString()).isEqualTo("2024-02-01");
+        
assertThat(result.get(1).periodDueDate().toString()).isEqualTo("2024-03-01");
+        assertThat(result.get(2).periodNumber()).isEqualTo(3);
+        
assertThat(result.get(2).periodFromDate().toString()).isEqualTo("2024-03-01");
+        
assertThat(result.get(2).periodDueDate().toString()).isEqualTo("2024-04-01");
+        assertThat(result.get(3).periodNumber()).isEqualTo(4);
+        
assertThat(result.get(3).periodFromDate().toString()).isEqualTo("2024-04-01");
+        
assertThat(result.get(3).periodDueDate().toString()).isEqualTo("2024-05-01");
+    }
+
     @Test
     @WithSystemTimeZone(EUROPE_BERLIN_ID)
     @WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)

Reply via email to