You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by al...@apache.org on 2022/11/28 17:41:02 UTC
[fineract] 01/04: FINERACT-1746: Random Installments Generation Issue (#2629)
This is an automated email from the ASF dual-hosted git repository.
aleks pushed a commit to branch 1.7.2
in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 23fc45c545b3f043c31d4d09cc17b9efd9dddf30
Author: logoutdhaval <10...@users.noreply.github.com>
AuthorDate: Sat Oct 15 13:18:12 2022 +0530
FINERACT-1746: Random Installments Generation Issue (#2629)
Co-authored-by: Dhaval Maniyar <dh...@Dhavals-MacBook-Pro.local>
---
.../loanschedule/data/LoanScheduleParams.java | 73 +++---
.../domain/AbstractLoanScheduleGenerator.java | 15 +-
.../ClientLoanIntegrationTest.java | 283 +++++++++++++++++++++
.../common/loans/LoanProductTestBuilder.java | 9 +
.../common/loans/LoanTransactionHelper.java | 5 +
5 files changed, 350 insertions(+), 35 deletions(-)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java
index c5c0bec83..1e18a9608 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanScheduleParams.java
@@ -25,6 +25,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+import lombok.Getter;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@@ -40,6 +41,8 @@ public final class LoanScheduleParams {
private LocalDate periodStartDate;
private LocalDate actualRepaymentDate;
+ @Getter
+ private LocalDate originalRepaymentDate;
// variables for cumulative totals
private Money totalCumulativePrincipal;
@@ -100,13 +103,13 @@ public final class LoanScheduleParams {
private final boolean applyInterestRecalculation;
private LoanScheduleParams(final int periodNumber, final int instalmentNumber, int loanTermInDays, LocalDate periodStartDate,
- final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, final Money totalCumulativeInterest,
- final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, final Money totalRepaymentExpected,
- Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal, final Map<LocalDate, Money> principalPortionMap,
- final Map<LocalDate, Money> latePaymentMap, final Map<LocalDate, Money> compoundingMap, final Money unCompoundedAmount,
- final Map<LocalDate, Money> disburseDetailMap, Money principalToBeScheduled, final Money outstandingBalance,
- final Money outstandingBalanceAsPerRest, final List<LoanRepaymentScheduleInstallment> installments,
- final Collection<RecalculationDetail> recalculationDetails,
+ final LocalDate actualRepaymentDate, final LocalDate originalRepaymentDate, final Money totalCumulativePrincipal,
+ final Money totalCumulativeInterest, final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged,
+ final Money totalRepaymentExpected, Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal,
+ final Map<LocalDate, Money> principalPortionMap, final Map<LocalDate, Money> latePaymentMap,
+ final Map<LocalDate, Money> compoundingMap, final Money unCompoundedAmount, final Map<LocalDate, Money> disburseDetailMap,
+ Money principalToBeScheduled, final Money outstandingBalance, final Money outstandingBalanceAsPerRest,
+ final List<LoanRepaymentScheduleInstallment> installments, final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final boolean partialUpdate, final MonetaryCurrency currency, final boolean applyInterestRecalculation) {
this.periodNumber = periodNumber;
@@ -114,6 +117,7 @@ public final class LoanScheduleParams {
this.loanTermInDays = loanTermInDays;
this.periodStartDate = periodStartDate;
this.actualRepaymentDate = actualRepaymentDate;
+ this.originalRepaymentDate = originalRepaymentDate;
this.totalCumulativePrincipal = totalCumulativePrincipal;
this.totalCumulativeInterest = totalCumulativeInterest;
this.totalFeeChargesCharged = totalFeeChargesCharged;
@@ -142,10 +146,10 @@ public final class LoanScheduleParams {
}
public static LoanScheduleParams createLoanScheduleParamsForPartialUpdate(final int periodNumber, final int instalmentNumber,
- int loanTermInDays, LocalDate periodStartDate, final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal,
- final Money totalCumulativeInterest, final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged,
- final Money totalRepaymentExpected, Money totalOutstandingInterestPaymentDueToGrace, final Money reducePrincipal,
- final Map<LocalDate, Money> principalPortionMap, final Map<LocalDate, Money> latePaymentMap,
+ int loanTermInDays, LocalDate periodStartDate, final LocalDate actualRepaymentDate, final LocalDate originalRepaymentDate,
+ final Money totalCumulativePrincipal, final Money totalCumulativeInterest, final Money totalFeeChargesCharged,
+ final Money totalPenaltyChargesCharged, final Money totalRepaymentExpected, Money totalOutstandingInterestPaymentDueToGrace,
+ final Money reducePrincipal, final Map<LocalDate, Money> principalPortionMap, final Map<LocalDate, Money> latePaymentMap,
final Map<LocalDate, Money> compoundingMap, Money unCompoundedAmount, final Map<LocalDate, Money> disburseDetailMap,
final Money principalToBeScheduled, final Money outstandingBalance, final Money outstandingBalanceAsPerRest,
final List<LoanRepaymentScheduleInstallment> installments, final Collection<RecalculationDetail> recalculationDetails,
@@ -153,11 +157,11 @@ public final class LoanScheduleParams {
final MonetaryCurrency currency, final boolean applyInterestRecalculation) {
final boolean partialUpdate = true;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
- totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
- totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
- compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
- outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
- scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
+ originalRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged,
+ totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal,
+ principalPortionMap, latePaymentMap, compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled,
+ outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails,
+ loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public static LoanScheduleParams createLoanScheduleParamsForCompleteUpdate(final Collection<RecalculationDetail> recalculationDetails,
@@ -167,6 +171,7 @@ public final class LoanScheduleParams {
final int instalmentNumber = 1;
final LocalDate periodStartDate = null;
final LocalDate actualRepaymentDate = null;
+ final LocalDate originalRepaymentDate = null;
final Money totalCumulativePrincipal = null;
final Money totalCumulativeInterest = null;
final Money totalFeeChargesCharged = null;
@@ -187,11 +192,11 @@ public final class LoanScheduleParams {
final MonetaryCurrency currency = null;
final Money unCompoundedAmount = null;
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
- totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
- totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
- compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
- outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
- scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
+ originalRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged,
+ totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal,
+ principalPortionMap, latePaymentMap, compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled,
+ outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails,
+ loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement,
@@ -202,6 +207,7 @@ public final class LoanScheduleParams {
final Money totalCumulativePrincipal = Money.zero(currency);
final Money totalCumulativeInterest = Money.zero(currency);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
+ final LocalDate originalRepaymentDate = periodStartDate;
final LocalDate actualRepaymentDate = periodStartDate;
final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement;
final Money totalPenaltyChargesCharged = Money.zero(currency);
@@ -221,11 +227,11 @@ public final class LoanScheduleParams {
final boolean applyInterestRecalculation = false;
final Money unCompoundedAmount = Money.zero(currency);
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
- totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
- totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
- compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
- outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
- scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
+ originalRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged,
+ totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal,
+ principalPortionMap, latePaymentMap, compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled,
+ outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails,
+ loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public static LoanScheduleParams createLoanScheduleParams(final MonetaryCurrency currency, final Money chargesDueAtTimeOfDisbursement,
@@ -236,6 +242,7 @@ public final class LoanScheduleParams {
final Money totalCumulativePrincipal = Money.zero(currency);
final Money totalCumulativeInterest = Money.zero(currency);
final Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
+ final LocalDate originalRepaymentDate = periodStartDate;
final LocalDate actualRepaymentDate = periodStartDate;
final Money totalFeeChargesCharged = chargesDueAtTimeOfDisbursement;
final Money totalPenaltyChargesCharged = Money.zero(currency);
@@ -255,11 +262,11 @@ public final class LoanScheduleParams {
final boolean applyInterestRecalculation = loanScheduleParams.applyInterestRecalculation;
final Money unCompoundedAmount = Money.zero(currency);
return new LoanScheduleParams(periodNumber, instalmentNumber, loanTermInDays, periodStartDate, actualRepaymentDate,
- totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
- totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap,
- compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance,
- outstandingBalanceAsPerRest, installments, recalculationDetails, loanRepaymentScheduleTransactionProcessor,
- scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
+ originalRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged,
+ totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstandingInterestPaymentDueToGrace, reducePrincipal,
+ principalPortionMap, latePaymentMap, compoundingMap, unCompoundedAmount, disburseDetailMap, principalToBeScheduled,
+ outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails,
+ loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate, currency, applyInterestRecalculation);
}
public int getPeriodNumber() {
@@ -390,6 +397,10 @@ public final class LoanScheduleParams {
this.actualRepaymentDate = actualRepaymentDate;
}
+ public void setOriginalRepaymentDate(LocalDate originalRepaymentDate) {
+ this.originalRepaymentDate = originalRepaymentDate;
+ }
+
public void setReducePrincipal(Money reducePrincipal) {
this.reducePrincipal = reducePrincipal;
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java
index 4f906e76e..4894cc06f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java
@@ -2129,6 +2129,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
if (loanApplicationTerms.isInterestRecalculationEnabled()) {
lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO);
}
+
LocalDate actualRepaymentDate = loanApplicationTerms.getExpectedDisbursementDate();
boolean isFirstRepayment = true;
@@ -2144,6 +2145,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// Actual period Number plus interest only repayments
int instalmentNumber = 1;
LocalDate lastInstallmentDate = actualRepaymentDate;
+ LocalDate originalRepaymentDate = lastInstallmentDate;
LocalDate periodStartDate = loanApplicationTerms.getExpectedDisbursementDate();
// Set fixed Amortization Amounts(either EMI or Principal )
updateAmortization(mc, loanApplicationTerms, periodNumber, outstandingBalance);
@@ -2193,15 +2195,19 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate,
loanApplicationTerms, isFirstRepayment);
if (actualRepaymentDate.isAfter(rescheduleFrom) || actualRepaymentDate.isEqual(rescheduleFrom)) {
- actualRepaymentDate = lastInstallmentDate;
+ actualRepaymentDate = originalRepaymentDate;
}
isFirstRepayment = false;
LocalDate prevLastInstDate = lastInstallmentDate;
+ originalRepaymentDate = actualRepaymentDate.plusMonths(1);
lastInstallmentDate = this.scheduledDateGenerator
.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
LocalDate modifiedLastInstDate = null;
LoanTermVariationsData variation1 = null;
+
+ boolean hasDueDateVariation = false;
while (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(lastInstallmentDate)) {
+ hasDueDateVariation = true;
LoanTermVariationsData variation = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation();
if (!variation.isSpecificToInstallment()) {
modifiedLastInstDate = variation.getDateValue();
@@ -2209,7 +2215,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
}
- if (!lastInstallmentDate.isEqual(installment.getDueDate())
+ if (hasDueDateVariation && !lastInstallmentDate.isEqual(installment.getDueDate())
&& !installment.getDueDate().equals(modifiedLastInstDate)) {
lastInstallmentDate = prevLastInstDate;
actualRepaymentDate = lastInstallmentDate;
@@ -2347,8 +2353,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
if (!newRepaymentScheduleInstallments.isEmpty() && totalCumulativeInterest.isGreaterThanZero()) {
Money totalOutstandingInterestPaymentDueToGrace = Money.zero(currency);
loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForPartialUpdate(periodNumber, instalmentNumber,
- loanTermInDays, periodStartDate, actualRepaymentDate, totalCumulativePrincipal, totalCumulativeInterest,
- totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected,
+ loanTermInDays, periodStartDate, actualRepaymentDate, originalRepaymentDate, totalCumulativePrincipal,
+ totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged, totalRepaymentExpected,
totalOutstandingInterestPaymentDueToGrace, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap,
uncompoundedAmount, disburseDetailMap, principalToBeScheduled, outstandingBalance, outstandingBalanceAsPerRest,
newRepaymentScheduleInstallments, recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate,
@@ -2356,6 +2362,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
retainedInstallments.addAll(newRepaymentScheduleInstallments);
loanScheduleParams.getCompoundingDateVariations().putAll(compoundingDateVariations);
loanApplicationTerms.updateTotalInterestDue(Money.of(currency, loan.getLoanSummary().getTotalInterestCharged()));
+ loanScheduleParams.setOriginalRepaymentDate(originalRepaymentDate);
} else {
loanApplicationTerms.getLoanTermVariations().resetVariations();
}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
index b5324799b..22bae64a3 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
@@ -32,6 +32,7 @@ import io.restassured.specification.ResponseSpecification;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.DecimalFormat;
+import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -45,8 +46,10 @@ import java.util.Map;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.WorkingDaysHelper;
import org.apache.fineract.integrationtests.common.accounting.Account;
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.accounting.JournalEntry;
@@ -4602,6 +4605,20 @@ public class ClientLoanIntegrationTest {
recalculationRestFrequencyOnDayType, recalculationRestFrequencyDayOfWeekType);
}
+ private Integer createLoanProductWithInterestRecalculationAndCompoundingDetails(final String repaymentStrategy,
+ final String interestRecalculationCompoundingMethod, final String rescheduleStrategyMethod,
+ final String recalculationRestFrequencyType, final String preCloseInterestCalculationStrategy, final Account[] accounts,
+ final String installmentMultipleOf) {
+ final String recalculationCompoundingFrequencyType = null;
+ final String recalculationCompoundingFrequencyInterval = null;
+ final Integer recalculationCompoundingFrequencyOnDayType = null;
+ final Integer recalculationCompoundingFrequencyDayOfWeekType = null;
+ return createLoanProductWithInterestRecalculation(repaymentStrategy, interestRecalculationCompoundingMethod,
+ rescheduleStrategyMethod, recalculationCompoundingFrequencyType, recalculationCompoundingFrequencyInterval,
+ preCloseInterestCalculationStrategy, accounts, null, false, recalculationCompoundingFrequencyOnDayType,
+ recalculationCompoundingFrequencyDayOfWeekType, installmentMultipleOf);
+ }
+
private Integer createLoanProductWithInterestRecalculation(final String repaymentStrategy,
final String interestRecalculationCompoundingMethod, final String rescheduleStrategyMethod,
final String recalculationRestFrequencyType, final String recalculationRestFrequencyInterval,
@@ -4636,6 +4653,37 @@ public class ClientLoanIntegrationTest {
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
}
+ private Integer createLoanProductWithInterestRecalculation(final String repaymentStrategy,
+ final String interestRecalculationCompoundingMethod, final String rescheduleStrategyMethod,
+ final String recalculationCompoundingFrequencyType, final String recalculationCompoundingFrequencyInterval,
+ final String preCloseInterestCalculationStrategy, final Account[] accounts, final String chargeId,
+ boolean isArrearsBasedOnOriginalSchedule, final Integer recalculationCompoundingFrequencyOnDayType,
+ final Integer recalculationCompoundingFrequencyDayOfWeekType, final String installmentsMultiplesOf) {
+ LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+ LoanProductTestBuilder builder = new LoanProductTestBuilder().withPrincipal("10000.00").withNumberOfRepayments("12")
+ .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("19.9")
+ .withInterestRateFrequencyTypeAsMonths().withRepaymentStrategy(repaymentStrategy).withAmortizationTypeAsEqualInstallments()
+ .withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeAsDays()
+ .withInterestRecalculationDetails(interestRecalculationCompoundingMethod, rescheduleStrategyMethod,
+ preCloseInterestCalculationStrategy)
+ .withInterestRecalculationDetails(interestRecalculationCompoundingMethod, rescheduleStrategyMethod,
+ preCloseInterestCalculationStrategy)
+ .withInterestRecalculationCompoundingFrequencyDetails(recalculationCompoundingFrequencyType,
+ recalculationCompoundingFrequencyInterval, recalculationCompoundingFrequencyOnDayType,
+ recalculationCompoundingFrequencyDayOfWeekType)
+ .withDefineInstallmentAmount(true).withInstallmentAmountInMultiplesOf(installmentsMultiplesOf);
+ if (accounts != null) {
+ builder = builder.withAccountingRulePeriodicAccrual(accounts);
+ }
+
+ if (isArrearsBasedOnOriginalSchedule) {
+ builder = builder.withArrearsConfiguration();
+ }
+
+ final String loanProductJSON = builder.build(chargeId);
+ return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
+ }
+
private Integer applyForLoanApplicationForInterestRecalculation(final Integer clientID, final Integer loanProductID,
final String disbursementDate, final String repaymentStrategy, final List<HashMap> charges) {
return applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, disbursementDate, repaymentStrategy, charges, null,
@@ -4689,6 +4737,37 @@ public class ClientLoanIntegrationTest {
return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
}
+ private Integer applyForLoanApplicationForInterestRecalculation(final Integer clientID, final Integer loanProductID,
+ final String disbursementDate, final String repaymentStrategy, final String firstRepaymentDate) {
+ LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+ final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec);
+ Assertions.assertNotNull(collateralId);
+ List<HashMap> collaterals = new ArrayList<>();
+
+ final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec,
+ String.valueOf(clientID), collateralId);
+ Assertions.assertNotNull(clientCollateralId);
+ addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1));
+
+ final String loanApplicationJSON = new LoanApplicationTestBuilder() //
+ .withPrincipal("10000.00") //
+ .withLoanTermFrequency("12") //
+ .withLoanTermFrequencyAsMonths() //
+ .withNumberOfRepayments("12") //
+ .withRepaymentEveryAfter("1") //
+ .withLoanTermFrequencyAsMonths() //
+ .withInterestRatePerPeriod("19.9") //
+ .withAmortizationTypeAsEqualInstallments() //
+ .withInterestTypeAsDecliningBalance() //
+ .withInterestCalculationPeriodTypeAsDays() //
+ .withExpectedDisbursementDate(disbursementDate) //
+ .withSubmittedOnDate(disbursementDate) //
+ .withwithRepaymentStrategy(repaymentStrategy).withRepaymentFrequencyTypeAsMonths()//
+ .withFirstRepaymentDate(firstRepaymentDate).withCollaterals(collaterals)
+ .build(clientID.toString(), loanProductID.toString(), null);
+ return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+ }
+
private void verifyLoanRepaymentSchedule(final ArrayList<HashMap> loanSchedule, List<Map<String, Object>> expectedvalues) {
int index = 1;
verifyLoanRepaymentSchedule(loanSchedule, expectedvalues, index);
@@ -5673,6 +5752,210 @@ public class ClientLoanIntegrationTest {
assertEquals(clientCollateralId, clientCollateralIdResult);
}
+ @Test
+ public void testLoanScheduleWithInterestRecalculationMakePrepaymentAfterRepayment() {
+ this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+ DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", Locale.US);
+ dateFormat.setTimeZone(Utils.getTimeZoneOfTenant());
+ GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", true);
+ Calendar startDate = Calendar.getInstance(Utils.getTimeZoneOfTenant());
+ Calendar currentDate = Calendar.getInstance(Utils.getTimeZoneOfTenant());
+ startDate.add(Calendar.MONTH, -8);
+
+ Calendar firstRepaymentDate = (Calendar) startDate.clone();
+ firstRepaymentDate.add(Calendar.MONTH, 1);
+ firstRepaymentDate.add(Calendar.DAY_OF_MONTH, firstRepaymentDate.getActualMaximum(Calendar.DAY_OF_MONTH) - Calendar.DAY_OF_MONTH);
+ String firstRepayment = dateFormat.format(firstRepaymentDate.getTime());
+
+ final String loanDisbursementDate = dateFormat.format(startDate.getTime());
+ final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+ ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
+ final Integer loanProductID = createLoanProductWithInterestRecalculationAndCompoundingDetails(
+ LoanProductTestBuilder.INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY,
+ LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE,
+ LoanProductTestBuilder.RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS,
+ LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_SAME_AS_REPAYMENT_PERIOD,
+ LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE, null, "12");
+
+ final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, loanDisbursementDate,
+ LoanApplicationTestBuilder.INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY, firstRepayment);
+
+ Assertions.assertNotNull(loanID);
+ HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
+ LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+ LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
+ loanStatusHashMap = this.loanTransactionHelper.approveLoan(loanDisbursementDate, loanID);
+ LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+ LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+ LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------");
+ String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
+ loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(loanDisbursementDate, loanID,
+ JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+ LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+ ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+ Assertions.assertNotNull(loanSchedule);
+ startDate.add(Calendar.DAY_OF_MONTH, 2);
+ String loanFirstRepaymentDate = dateFormat.format(startDate.getTime());
+ //
+ Float earlyPayment = Float.parseFloat("3000");
+ this.loanTransactionHelper.makeRepayment(loanFirstRepaymentDate, earlyPayment, loanID);
+
+ HashMap prepayDetail = this.loanTransactionHelper.getPrepayAmount(this.requestSpec, this.responseSpec, loanID);
+ String prepayAmount = String.valueOf(prepayDetail.get("amount"));
+ String loanPrepaymentDate = dateFormat.format(currentDate.getTime());
+ this.loanTransactionHelper.makeRepayment(loanPrepaymentDate, Float.parseFloat(prepayAmount), loanID);
+ loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
+ LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap);
+ }
+
+ @Test
+ public void testLoanScheduleWithInterestRecalculationMakeAdvancePaymentTillSettlement() {
+ this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+ final ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build();
+ final LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.requestSpec, errorResponse);
+ DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", Locale.US);
+ dateFormat.setTimeZone(Utils.getTimeZoneOfTenant());
+ GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", true);
+ Calendar startDate = Calendar.getInstance(Utils.getTimeZoneOfTenant());
+ Calendar currentDate = Calendar.getInstance(Utils.getTimeZoneOfTenant());
+ startDate.add(Calendar.MONTH, -8);
+
+ Calendar firstRepaymentDate = (Calendar) startDate.clone();
+ firstRepaymentDate.add(Calendar.MONTH, 1);
+ firstRepaymentDate.add(Calendar.DAY_OF_MONTH, firstRepaymentDate.getActualMaximum(Calendar.DAY_OF_MONTH) - Calendar.DAY_OF_MONTH);
+ String firstRepayment = dateFormat.format(firstRepaymentDate.getTime());
+
+ final String loanDisbursementDate = dateFormat.format(startDate.getTime());
+ final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+ ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
+ final Integer loanProductID = createLoanProductWithInterestRecalculationAndCompoundingDetails(
+ LoanProductTestBuilder.INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY,
+ LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE,
+ LoanProductTestBuilder.RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS,
+ LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_SAME_AS_REPAYMENT_PERIOD,
+ LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE, null, "12");
+
+ final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, loanDisbursementDate,
+ LoanApplicationTestBuilder.INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY, firstRepayment);
+
+ Assertions.assertNotNull(loanID);
+ HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
+ LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+ LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
+ loanStatusHashMap = this.loanTransactionHelper.approveLoan(loanDisbursementDate, loanID);
+ LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+ LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+ LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------");
+ String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
+ loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(loanDisbursementDate, loanID,
+ JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+ LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+ ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+ Assertions.assertNotNull(loanSchedule);
+ Calendar repaymentDate = (Calendar) firstRepaymentDate.clone();
+ startDate.add(Calendar.DAY_OF_MONTH, 2);
+ String loanFirstRepaymentDate = dateFormat.format(startDate.getTime());
+ //
+ Float earlyPayment = Float.parseFloat("3000");
+ String retrieveDueDate = null;
+ Float amount = null;
+ this.loanTransactionHelper.makeRepayment(loanFirstRepaymentDate, earlyPayment, loanID);
+ for (int i = 1; i < loanSchedule.size(); i++) {
+
+ retrieveDueDate = dateFormat.format(repaymentDate.getTime());
+ amount = (Float) loanSchedule.get(i).get("principalOriginalDue") + (Float) loanSchedule.get(i).get("interestOriginalDue");
+ if (currentDate.after(repaymentDate)) {
+ this.loanTransactionHelper.makeRepayment(retrieveDueDate, amount, loanID);
+ } else {
+ break;
+ }
+ repaymentDate.add(Calendar.MONTH, 1);
+ }
+ HashMap savingsAccountErrorData = validationErrorHelper.makeRepayment(retrieveDueDate, amount, loanID);
+ ArrayList<HashMap> error = (ArrayList<HashMap>) savingsAccountErrorData.get("errors");
+ assertEquals("error.msg.loan.transaction.cannot.be.a.future.date", error.get(0).get("userMessageGlobalisationCode"));
+ }
+
+ @Test
+ public void testLoanScheduleWithInterestRecalculationAfterLatePayment() {
+ this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+ WorkingDaysHelper.updateWorkingDaysWeekDays(this.requestSpec, this.responseSpec);
+ DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", Locale.US);
+ dateFormat.setTimeZone(Utils.getTimeZoneOfTenant());
+ GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", true);
+
+ final String loanDisbursementDate = "28 January 2021";
+ String firstRepayment = "01 March 2021";
+ final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+ ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
+ final Integer loanProductID = createLoanProductWithInterestRecalculationAndCompoundingDetails(
+ LoanProductTestBuilder.INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY,
+ LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE,
+ LoanProductTestBuilder.RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS,
+ LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_SAME_AS_REPAYMENT_PERIOD,
+ LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE, null, "12");
+
+ final Integer loanID = applyForLoanApplicationForInterestRecalculation(clientID, loanProductID, loanDisbursementDate,
+ LoanApplicationTestBuilder.INTEREST_PRINCIPAL_PENALTIES_FEES_ORDER_STRATEGY, firstRepayment);
+
+ Assertions.assertNotNull(loanID);
+ HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
+ LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+ LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
+ loanStatusHashMap = this.loanTransactionHelper.approveLoan(loanDisbursementDate, loanID);
+ LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+ LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+ LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------");
+ String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
+ loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(loanDisbursementDate, loanID,
+ JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+ LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+ ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+ Assertions.assertNotNull(loanSchedule);
+
+ List<Map<String, Object>> expectedvalues = new ArrayList<>();
+ addRepaymentValues(expectedvalues, convertStringDateToCalender("01 March 2021"), 0, false, "388.11", "1831.89", "0.0", "0.0");
+ this.loanTransactionHelper.makeRepayment("01 March 2021", 2220.0F, loanID);
+
+ addRepaymentValues(expectedvalues, convertStringDateToCalender("01 April 2021"), 0, false, "270.55", "1949.45", "0.0", "0.0");
+ this.loanTransactionHelper.makeRepayment("01 April 2021", 2220.0F, loanID);
+
+ addRepaymentValues(expectedvalues, convertStringDateToCalender("03 May 2021"), 0, false, "264.31", "1955.69", "0.0", "0.0");
+ this.loanTransactionHelper.makeRepayment("04 May 2021", 2220.0F, loanID);
+
+ addRepaymentValues(expectedvalues, convertStringDateToCalender("04 May 2021"), 0, false, "0.0", "61.12", "0.0", "0.0");
+ this.loanTransactionHelper.makeRepayment("01 June 2021", 2220.0F, loanID);
+
+ addRepaymentValues(expectedvalues, convertStringDateToCalender("01 June 2021"), 0, false, "496.07", "1662.81", "0.0", "0.0");
+ addRepaymentValues(expectedvalues, convertStringDateToCalender("01 July 2021"), 0, false, "535.78", "1684.22", "0.0", "0.0");
+ loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+ Assertions.assertNotNull(loanSchedule);
+ verifyLoanRepaymentSchedule(loanSchedule, expectedvalues);
+ WorkingDaysHelper.updateWorkingDays(this.requestSpec, this.responseSpec);
+ }
+
+ private Calendar convertStringDateToCalender(final String stringDate) {
+ DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", Locale.US);
+ Calendar date = Calendar.getInstance();
+ try {
+ Date date1 = dateFormat.parse(stringDate);
+ date.setTime(date1);
+
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ return date;
+ }
+
private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) {
String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID);
String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
index 7f0e0f8b8..ecd5aa2e7 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
@@ -126,6 +126,7 @@ public class LoanProductTestBuilder {
private boolean syncExpectedWithDisbursementDate = false;
private String fixedPrincipalPercentagePerInstallment;
private String installmentAmountInMultiplesOf;
+ private boolean canDefineInstallmentAmount;
public String build(final String chargeId) {
final HashMap<String, Object> map = new HashMap<>();
@@ -163,6 +164,9 @@ public class LoanProductTestBuilder {
if (this.minimumDaysBetweenDisbursalAndFirstRepayment != null) {
map.put("minimumDaysBetweenDisbursalAndFirstRepayment", this.minimumDaysBetweenDisbursalAndFirstRepayment);
}
+ if (this.canDefineInstallmentAmount) {
+ map.put("canDefineInstallmentAmount", this.canDefineInstallmentAmount);
+ }
if (multiDisburseLoan) {
map.put("multiDisburseLoan", this.multiDisburseLoan);
map.put("maxTrancheCount", this.maxTrancheCount);
@@ -443,6 +447,11 @@ public class LoanProductTestBuilder {
return this;
}
+ public LoanProductTestBuilder withDefineInstallmentAmount(final boolean canDefineInstallmentAmount) {
+ this.canDefineInstallmentAmount = canDefineInstallmentAmount;
+ return this;
+ }
+
public LoanProductTestBuilder withInterestRecalculationDetails(final String interestRecalculationCompoundingMethod,
final String rescheduleStrategyMethod, String preCloseInterestCalculationStrategy) {
this.isInterestRecalculationEnabled = true;
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 85527944d..36fbfdca2 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -298,6 +298,11 @@ public class LoanTransactionHelper {
return performLoanTransaction(createGlimAccountURL(DISBURSE_LOAN_COMMAND, glimID), getDisbursementAsJSON(date));
}
+ public HashMap disburseLoanWithNetDisbursalAmount(final String date, final Integer loanID, final String netDisbursalAmount) {
+ return performLoanTransaction(createLoanOperationURL(DISBURSE_LOAN_COMMAND, loanID),
+ getDisburseLoanAsJSON(date, null, netDisbursalAmount));
+ }
+
public HashMap undoDisburseGlimAccount(final Integer glimID) {
LOG.info("--------------------------------- UNDO DISBURSAL GLIM APPLICATION -------------------------------");
final String undoBodyJson = "{'note':'UNDO DISBURSAL'}";