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 <[email protected]> AuthorDate: Sat Oct 15 13:18:12 2022 +0530 FINERACT-1746: Random Installments Generation Issue (#2629) Co-authored-by: Dhaval Maniyar <[email protected]> --- .../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'}";
