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 c85400f760 FINERACT-2357: Support moratorium for progressive loans
c85400f760 is described below
commit c85400f760b7a07b1f002e040f65d878e48e98d4
Author: airajena <[email protected]>
AuthorDate: Wed Feb 4 16:59:22 2026 +0530
FINERACT-2357: Support moratorium for progressive loans
---
.../mapper/LoanConfigurationDetailsMapper.java | 2 +-
.../loanproduct/calc/ProgressiveEMICalculator.java | 66 ++++++++++++++++----
.../data/LoanInterestScheduleModelModifiers.java | 3 +-
.../data/ProgressiveLoanInterestScheduleModel.java | 15 ++++-
.../loanproduct/calc/data/RepaymentPeriod.java | 10 ++-
.../calc/ProgressiveEMICalculatorTest.java | 65 +++++++++++++++++++
.../ProgressiveLoanMoratoriumIntegrationTest.java | 72 ++++++++++++++++++++++
7 files changed, 216 insertions(+), 17 deletions(-)
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
index 0254a6a51a..6a6896b4a8 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanConfigurationDetailsMapper.java
@@ -48,7 +48,7 @@ public final class LoanConfigurationDetailsMapper {
return new LoanConfigurationDetails(currencyData,
loanProductRelatedDetail.getNominalInterestRatePerPeriod(),
loanProductRelatedDetail.getAnnualNominalInterestRate(),
loanProductRelatedDetail.getGraceOnInterestCharged(),
- loanProductRelatedDetail.getGraceOnPrincipalPayment(),
loanProductRelatedDetail.getGraceOnPrincipalPayment(),
+ loanProductRelatedDetail.getGraceOnInterestPayment(),
loanProductRelatedDetail.getGraceOnPrincipalPayment(),
loanProductRelatedDetail.getRecurringMoratoriumOnPrincipalPeriods(),
loanProductRelatedDetail.getInterestMethod(),
loanProductRelatedDetail.getInterestCalculationPeriodMethod(),
DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType()),
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
index c2e1546893..085387e067 100644
---
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
@@ -658,11 +658,13 @@ public final class ProgressiveEMICalculator implements
EMICalculator {
private void calculateEMIValueAndRateFactorsForFlatInterestMethod(final
LocalDate calculateFromRepaymentPeriodDueDate,
final ProgressiveLoanInterestScheduleModel scheduleModel, final
EmiChangeOperation operation) {
final List<RepaymentPeriod> relatedRepaymentPeriods =
scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
+ applyInterestMoratoriumIfRequired(scheduleModel);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
if (relatedRepaymentPeriods.isEmpty()) {
return;
}
calculateEMIOnActualModelWithFlatInterestMethod(relatedRepaymentPeriods,
scheduleModel);
+ applyPrincipalMoratoriumIfRequired(relatedRepaymentPeriods,
scheduleModel);
}
/**
@@ -687,6 +689,7 @@ public final class ProgressiveEMICalculator implements
EMICalculator {
|| operation.getAction() ==
EmiChangeOperation.Action.INTEREST_RATE_CHANGE
|| operation.getAction() ==
EmiChangeOperation.Action.ADD_REPAYMENT_PERIODS || scheduleModel.isCopy();
+ applyInterestMoratoriumIfRequired(scheduleModel);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
calculateOutstandingBalance(scheduleModel);
if (onlyOnActualModelShouldApply) {
@@ -694,6 +697,7 @@ public final class ProgressiveEMICalculator implements
EMICalculator {
} else {
calculateEMIOnNewModelAndMerge(relatedRepaymentPeriods,
scheduleModel, operation);
}
+ applyPrincipalMoratoriumIfRequired(relatedRepaymentPeriods,
scheduleModel);
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel,
calculateFromRepaymentPeriodDueDate);
if (onlyOnActualModelShouldApply) {
@@ -1628,11 +1632,48 @@ public final class ProgressiveEMICalculator implements
EMICalculator {
}
}
+ private void applyPrincipalMoratoriumIfRequired(List<RepaymentPeriod>
repaymentPeriods,
+ ProgressiveLoanInterestScheduleModel scheduleModel) {
+ if (repaymentPeriods.isEmpty()) {
+ return;
+ }
+ Integer graceOnPrincipalPayment =
scheduleModel.loanProductRelatedDetail().getGraceOnPrincipalPayment();
+ if (graceOnPrincipalPayment == null || graceOnPrincipalPayment <= 0) {
+ return;
+ }
+ int gracePeriods = Math.min(graceOnPrincipalPayment,
repaymentPeriods.size());
+ List<RepaymentPeriod> gracePeriodsList = repaymentPeriods.subList(0,
gracePeriods);
+ gracePeriodsList.forEach(period -> {
+ Money interestOnlyEmi = period.getDueInterest();
+ period.setEmi(interestOnlyEmi);
+ period.setOriginalEmi(interestOnlyEmi);
+ });
+ if (gracePeriods == repaymentPeriods.size()) {
+ return;
+ }
+ calculateOutstandingBalance(scheduleModel);
+ calculateEMIOnActualModel(repaymentPeriods.subList(gracePeriods,
repaymentPeriods.size()), scheduleModel);
+ }
+
+ private void applyInterestMoratoriumIfRequired(final
ProgressiveLoanInterestScheduleModel scheduleModel) {
+ if (!scheduleModel.isInterestPauseForEmiCalculationEnabled() ||
scheduleModel.repaymentPeriods().isEmpty()) {
+ return;
+ }
+ final Integer graceOnInterestPayment =
scheduleModel.loanProductRelatedDetail().getGraceOnInterestPayment();
+ if (graceOnInterestPayment == null || graceOnInterestPayment <= 0) {
+ return;
+ }
+ scheduleModel.repaymentPeriods().forEach(repaymentPeriod ->
repaymentPeriod.setInterestPaymentGrace(false));
+ final int gracePeriods = Math.min(graceOnInterestPayment,
scheduleModel.repaymentPeriods().size());
+ final List<RepaymentPeriod> gracePeriodsList =
scheduleModel.repaymentPeriods().subList(0, gracePeriods);
+ gracePeriodsList.forEach(repaymentPeriod ->
repaymentPeriod.setInterestPaymentGrace(true));
+ }
+
private void
calculateEMIOnActualModelWithDecliningBalanceInterestMethod(List<RepaymentPeriod>
repaymentPeriods,
ProgressiveLoanInterestScheduleModel scheduleModel) {
final MathContext mc = scheduleModel.mc();
- final BigDecimal rateFactorN =
MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods, mc));
- final BigDecimal fnResult =
MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
+ final BigDecimal rateFactorN =
MathUtil.stripTrailingZeros(calculateRateFactorPlus1NForEmi(repaymentPeriods,
scheduleModel, mc));
+ final BigDecimal fnResult =
MathUtil.stripTrailingZeros(calculateFnResultForEmi(repaymentPeriods,
scheduleModel, mc));
final RepaymentPeriod startPeriod = repaymentPeriods.getFirst();
final Money outstandingBalance =
startPeriod.getInitialBalanceForEmiRecalculation();
@@ -1724,24 +1765,25 @@ public final class ProgressiveEMICalculator implements
EMICalculator {
return Optional.empty();
}
- /**
- * Calculate Rate Factor Product from rate factors
- */
- private BigDecimal calculateRateFactorPlus1N(final List<RepaymentPeriod>
periods, MathContext mc) {
- return
periods.stream().map(RepaymentPeriod::getRateFactorPlus1).reduce(BigDecimal.ONE,
+ private BigDecimal calculateRateFactorPlus1NForEmi(final
List<RepaymentPeriod> periods,
+ final ProgressiveLoanInterestScheduleModel scheduleModel,
MathContext mc) {
+ return periods.stream().map(period -> getRateFactorPlus1ForEmi(period,
scheduleModel)).reduce(BigDecimal.ONE,
(BigDecimal acc, BigDecimal value) -> acc.multiply(value, mc));
}
- /**
- * Summarize Fn values
- */
- private BigDecimal calculateFnResult(final List<RepaymentPeriod> periods,
final MathContext mc) {
+ private BigDecimal calculateFnResultForEmi(final List<RepaymentPeriod>
periods,
+ final ProgressiveLoanInterestScheduleModel scheduleModel, final
MathContext mc) {
return periods.stream()//
.skip(1)//
- .map(RepaymentPeriod::getRateFactorPlus1)//
+ .map(period -> getRateFactorPlus1ForEmi(period,
scheduleModel))//
.reduce(BigDecimal.ONE, (previousFnValue, currentRateFactor)
-> fnValue(previousFnValue, currentRateFactor, mc));//
}
+ private BigDecimal getRateFactorPlus1ForEmi(final RepaymentPeriod period,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
+ return scheduleModel.isInterestPauseForEmiCalculationEnabled() &&
period.isInterestPaymentGrace() ? BigDecimal.ONE
+ : period.getRateFactorPlus1();
+ }
+
/**
* Calculate the EMI (Equal Monthly Installment) value
*/
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/LoanInterestScheduleModelModifiers.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/LoanInterestScheduleModelModifiers.java
index 3ebd0bcb25..79faa8ed0f 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/LoanInterestScheduleModelModifiers.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/LoanInterestScheduleModelModifiers.java
@@ -21,5 +21,6 @@ package org.apache.fineract.portfolio.loanproduct.calc.data;
public enum LoanInterestScheduleModelModifiers { //
EMI_RECALCULATION, //
COPY, //
- INTEREST_RECALCULATION_ENABLED //
+ INTEREST_RECALCULATION_ENABLED, //
+ INTEREST_PAUSE_FOR_EMI_CALCULATION //
}
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
index 1dabb865da..08bd958a23 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanproduct.calc.data;
import static
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
import static
org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.COPY;
import static
org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.EMI_RECALCULATION;
+import static
org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.INTEREST_PAUSE_FOR_EMI_CALCULATION;
import static
org.apache.fineract.portfolio.loanproduct.calc.data.LoanInterestScheduleModelModifiers.INTEREST_RECALCULATION_ENABLED;
import jakarta.validation.constraints.NotNull;
@@ -78,8 +79,11 @@ public class ProgressiveLoanInterestScheduleModel {
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.mc = mc;
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
+ final boolean interestPauseForEmiCalculation =
loanProductRelatedDetail.getGraceOnInterestPayment() != null
+ && loanProductRelatedDetail.getGraceOnInterestPayment() > 0;
modifiers = new HashMap<>(Map.of(EMI_RECALCULATION, true, COPY, false,
INTEREST_RECALCULATION_ENABLED,
- loanProductRelatedDetail.isInterestRecalculationEnabled()));
+ loanProductRelatedDetail.isInterestRecalculationEnabled(),
INTEREST_PAUSE_FOR_EMI_CALCULATION,
+ interestPauseForEmiCalculation));
}
private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod>
repaymentPeriods, final TreeSet<InterestRate> interestRates,
@@ -92,8 +96,11 @@ public class ProgressiveLoanInterestScheduleModel {
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
+ final boolean interestPauseForEmiCalculation =
loanProductRelatedDetail.getGraceOnInterestPayment() != null
+ && loanProductRelatedDetail.getGraceOnInterestPayment() > 0;
modifiers = new HashMap<>(Map.of(EMI_RECALCULATION, true, COPY,
isCopiedForCalculation, INTEREST_RECALCULATION_ENABLED,
- loanProductRelatedDetail.isInterestRecalculationEnabled()));
+ loanProductRelatedDetail.isInterestRecalculationEnabled(),
INTEREST_PAUSE_FOR_EMI_CALCULATION,
+ interestPauseForEmiCalculation));
}
public void recordOverdueCorrection(final LocalDate correctionDate, final
Money amount, final LocalDate affectedRpDueDate) {
@@ -446,6 +453,10 @@ public class ProgressiveLoanInterestScheduleModel {
return this.modifiers.get(COPY);
}
+ public boolean isInterestPauseForEmiCalculationEnabled() {
+ return this.modifiers.get(INTEREST_PAUSE_FOR_EMI_CALCULATION);
+ }
+
public Function<Long, LocalDate>
resolveRepaymentPeriodLengthGeneratorFunction(final LocalDate instance) {
return switch
(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
case MONTHS -> instance::plusMonths;
diff --git
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
index eca58ee334..0adaf82529 100644
---
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
+++
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java
@@ -78,6 +78,9 @@ public class RepaymentPeriod {
@Getter
@Setter
private boolean isInterestMovedUpward = false;
+ @Getter
+ @Setter
+ private boolean interestPaymentGrace = false;
@Setter
private Money totalDisbursedAmount;
@@ -158,6 +161,7 @@ public class RepaymentPeriod {
newRepaymentPeriod.setTotalDisbursedAmount(repaymentPeriod.getTotalDisbursedAmount());
newRepaymentPeriod.setTotalCapitalizedIncomeAmount(repaymentPeriod.getTotalCapitalizedIncomeAmount());
newRepaymentPeriod.setInterestMovedUpward(repaymentPeriod.isInterestMovedUpward());
+
newRepaymentPeriod.setInterestPaymentGrace(repaymentPeriod.isInterestPaymentGrace());
newRepaymentPeriod.setCurrency(repaymentPeriod.getCurrency());
// There is always at least 1 interest period, by default with same
from-due date as repayment period
for (InterestPeriod interestPeriod :
repaymentPeriod.getInterestPeriods()) {
@@ -180,6 +184,7 @@ public class RepaymentPeriod {
newRepaymentPeriod.setTotalDisbursedAmount(repaymentPeriod.getTotalDisbursedAmount());
newRepaymentPeriod.setTotalCapitalizedIncomeAmount(repaymentPeriod.getTotalCapitalizedIncomeAmount());
newRepaymentPeriod.setInterestMovedUpward(repaymentPeriod.isInterestMovedUpward());
+
newRepaymentPeriod.setInterestPaymentGrace(repaymentPeriod.isInterestPaymentGrace());
newRepaymentPeriod.setCurrency(repaymentPeriod.getCurrency());
// There is always at least 1 interest period, by default with same
from-due date as repayment period
for (InterestPeriod interestPeriod :
repaymentPeriod.getInterestPeriods()) {
@@ -265,6 +270,9 @@ public class RepaymentPeriod {
* @return
*/
public Money getDueInterest() {
+ if (isInterestPaymentGrace()) {
+ return getPaidInterest();
+ }
if (dueInterestCalculation == null) {
// Due interest might be the maximum paid if there is pay-off or
early repayment
dueInterestCalculation = Memo.of(
@@ -272,7 +280,7 @@ public class RepaymentPeriod {
: MathUtil.min(getCalculatedDueInterest(),
getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
getPaidInterest(), false),
() -> new Object[] { paidPrincipal, paidInterest,
interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount,
- fixedInterest, reAged, emi });
+ fixedInterest, reAged, emi, interestPaymentGrace
});
}
return dueInterestCalculation.get();
}
diff --git
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
index 491d10e452..c6b84d0461 100644
---
a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
+++
b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java
@@ -112,6 +112,8 @@ class ProgressiveEMICalculatorTest {
Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE);
Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY);
Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true);
+
Mockito.when(loanProductRelatedDetail.getGraceOnPrincipalPayment()).thenReturn(0);
+
Mockito.when(loanProductRelatedDetail.getGraceOnInterestPayment()).thenReturn(0);
}
private BigDecimal getRateFactorsByMonth(final DaysInYearType
daysInYearType, final DaysInMonthType daysInMonthType,
@@ -5207,6 +5209,69 @@ class ProgressiveEMICalculatorTest {
toCopy.installmentAmountInMultiplesOf());
}
+ @Test
+ public void test_principalGraceForProgressiveSchedule() {
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods
= new ArrayList<>();
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1),
LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1),
LocalDate.of(2024, 4, 1)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1),
LocalDate.of(2024, 5, 1)));
+ expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1),
LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1),
LocalDate.of(2024, 7, 1)));
+
+
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0));
+
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);
+
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(6);
+
Mockito.when(loanProductRelatedDetail.getGraceOnPrincipalPayment()).thenReturn(2);
+
Mockito.when(loanProductRelatedDetail.getGraceOnInterestPayment()).thenReturn(0);
+
+ final ProgressiveLoanInterestScheduleModel interestSchedule =
emiCalculator
+ .generatePeriodInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, null, mc);
+
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1,
1), toMoney(100.0));
+
+ checkPeriod(interestSchedule, 0, 0.58, 0.58, 0.0, 100.0, false);
+ checkPeriod(interestSchedule, 1, 0.58, 0.58, 0.0, 100.0, false);
+ checkPeriod(interestSchedule, 2, 25.37, 0.58, 24.79, 75.21, false);
+ checkPeriod(interestSchedule, 3, 25.37, 0.44, 24.93, 50.28, false);
+ checkPeriod(interestSchedule, 4, 25.37, 0.29, 25.08, 25.20, false);
+ checkPeriod(interestSchedule, 5, 25.35, 0.15, 25.20, 0.0, false);
+ }
+
+ @Test
+ public void test_interestGraceForProgressiveSchedule() {
+ final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods
= new ArrayList<>();
+ expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 2, 1)));
+ expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1),
LocalDate.of(2024, 3, 1)));
+ expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1),
LocalDate.of(2024, 4, 1)));
+ expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1),
LocalDate.of(2024, 5, 1)));
+ expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1),
LocalDate.of(2024, 6, 1)));
+ expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1),
LocalDate.of(2024, 7, 1)));
+
+
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0));
+
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);
+
Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(6);
+
Mockito.when(loanProductRelatedDetail.getGraceOnPrincipalPayment()).thenReturn(0);
+
Mockito.when(loanProductRelatedDetail.getGraceOnInterestPayment()).thenReturn(2);
+
+ final ProgressiveLoanInterestScheduleModel interestSchedule =
emiCalculator
+ .generatePeriodInterestScheduleModel(expectedRepaymentPeriods,
loanProductRelatedDetail, null, mc);
+
+ emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1,
1), toMoney(100.0));
+ checkPeriod(interestSchedule, 0, 17.01, 0.0, 17.01, 82.99, false);
+ checkPeriod(interestSchedule, 1, 17.01, 0.0, 17.01, 65.98, false);
+ checkPeriod(interestSchedule, 2, 17.01, 1.44, 15.57, 50.41, false);
+ checkPeriod(interestSchedule, 3, 17.01, 0.29, 16.72, 33.69, false);
+ checkPeriod(interestSchedule, 4, 17.01, 0.20, 16.81, 16.88, false);
+ checkPeriod(interestSchedule, 5, 16.98, 0.10, 16.88, 0.0, false);
+ }
+
@Test
public void
test_fullTermTranche_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month()
{
// Create 7 periods (6 original + 1 extension for second tranche)
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanMoratoriumIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanMoratoriumIntegrationTest.java
new file mode 100644
index 0000000000..9864c69cda
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ProgressiveLoanMoratoriumIntegrationTest.java
@@ -0,0 +1,72 @@
+/**
+ * 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.integrationtests;
+
+import java.math.BigDecimal;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.junit.jupiter.api.Test;
+
+public class ProgressiveLoanMoratoriumIntegrationTest extends
BaseLoanIntegrationTest {
+
+ @Test
+ public void testProgressivePrincipalMoratoriumSchedule() {
+ final PostClientsResponse client =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+ runAt("1 January 2024", () -> {
+ PostLoanProductsResponse loanProduct =
loanProductHelper.createLoanProduct(create4IProgressive().principal(100.0)
+
.minPrincipal(100.0).maxPrincipal(100.0).numberOfRepayments(6).interestRatePerPeriod(7.0));
+
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(),
loanProduct.getResourceId(), "1 January 2024", 100.0, 7.0, 6,
+ request -> request.graceOnPrincipalPayment(2));
+
+ disburseLoan(loanId, BigDecimal.valueOf(100.0), "1 January 2024");
+
+ verifyRepaymentSchedule(loanId, installment(100.0, null, "01
January 2024"),
+ installment(0.0, 0.58, 0.58, false, "01 February 2024"),
installment(0.0, 0.58, 0.58, false, "01 March 2024"), //
+ installment(24.79, 0.58, 25.37, false, "01 April 2024"), //
+ installment(24.93, 0.44, 25.37, false, "01 May 2024"), //
+ installment(25.08, 0.29, 25.37, false, "01 June 2024"), //
+ installment(25.20, 0.15, 25.35, false, "01 July 2024"));
+ });
+ }
+
+ @Test
+ public void testProgressiveInterestMoratoriumSchedule() {
+ final PostClientsResponse client =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+ runAt("1 January 2024", () -> {
+ PostLoanProductsResponse loanProduct =
loanProductHelper.createLoanProduct(create4IProgressive().principal(100.0)
+
.minPrincipal(100.0).maxPrincipal(100.0).numberOfRepayments(6).interestRatePerPeriod(7.0));
+
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(),
loanProduct.getResourceId(), "1 January 2024", 100.0, 7.0, 6,
+ request -> request.graceOnInterestPayment(2));
+
+ disburseLoan(loanId, BigDecimal.valueOf(100.0), "1 January 2024");
+
+ verifyRepaymentSchedule(loanId, installment(100.0, null, "01
January 2024"),
+ installment(17.01, 0.0, 17.01, false, "01 February 2024"),
installment(17.01, 0.0, 17.01, false, "01 March 2024"), //
+ installment(15.57, 1.44, 17.01, false, "01 April 2024"), //
+ installment(16.72, 0.29, 17.01, false, "01 May 2024"), //
+ installment(16.81, 0.20, 17.01, false, "01 June 2024"), //
+ installment(16.88, 0.10, 16.98, false, "01 July 2024"));
+ });
+ }
+}