diff --git a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index 6710ed2d3..cfd43a5ea 100644 --- a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -88,9 +88,9 @@ public class GlobalConfigurationHelper { ArrayList expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); ArrayList actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); - // There are currently 27 global configurations. - Assertions.assertEquals(28, expectedGlobalConfigurations.size()); - Assertions.assertEquals(28, actualGlobalConfigurations.size()); + // There are currently 29 global configurations. + Assertions.assertEquals(29, expectedGlobalConfigurations.size()); + Assertions.assertEquals(29, actualGlobalConfigurations.size()); for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { @@ -346,6 +346,14 @@ public class GlobalConfigurationHelper { enableSubRatesDefault.put("trapDoor", false); defaults.add(enableSubRatesDefault); + HashMap isFirstPaydayAllowedOnHoliday = new HashMap<>(); + isFirstPaydayAllowedOnHoliday.put("id", 33); + isFirstPaydayAllowedOnHoliday.put("name", "loan-reschedule-is-first-payday-allowed-on-holiday"); + isFirstPaydayAllowedOnHoliday.put("value", 0); + isFirstPaydayAllowedOnHoliday.put("enabled", false); + isFirstPaydayAllowedOnHoliday.put("trapDoor", false); + defaults.add(isFirstPaydayAllowedOnHoliday); + return defaults; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 65663b0c6..2632ae6ec 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -94,4 +94,6 @@ public interface ConfigurationDomainService { Integer retrieveOTPLiveTime(); boolean isSubRatesEnabled(); + + boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index efa1efa1c..a037f2ce3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -252,6 +252,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return getGlobalConfigurationPropertyData("skip-repayment-on-first-day-of-month").isEnabled(); } + @Override + public boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday() { + return getGlobalConfigurationPropertyData("loan-reschedule-is-first-payday-allowed-on-holiday").isEnabled(); + } + @Override public Long retreivePeroidInNumberOfDaysForSkipMeetingDate() { final String propertyName = "skip-repayment-on-first-day-of-month"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java index 8855ad03b..81571acc6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java @@ -43,6 +43,7 @@ public class ScheduleGeneratorDTO { final Integer numberOfdays; final boolean isSkipRepaymentOnFirstDayofMonth; final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; + final boolean isFirstRepaymentDateAllowedOnHoliday; public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO, @@ -50,7 +51,8 @@ public class ScheduleGeneratorDTO { final LocalDate recalculateFrom, final Long overdurPenaltyWaitPeriod, final FloatingRateDTO floatingRateDTO, final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays, - final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled) { + final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, + final boolean isFirstRepaymentDateAllowedOnHoliday) { this.loanScheduleFactory = loanScheduleFactory; this.applicationCurrency = applicationCurrency; @@ -67,6 +69,7 @@ public class ScheduleGeneratorDTO { this.numberOfdays = numberOfdays; this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth; this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; + this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; } public LoanScheduleGeneratorFactory getLoanScheduleFactory() { @@ -141,4 +144,8 @@ public class ScheduleGeneratorDTO { return this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; } + public boolean isFirstRepaymentDateAllowedOnHoliday() { + return isFirstRepaymentDateAllowedOnHoliday; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index b8a2850f3..a8158d466 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -5486,7 +5486,7 @@ public class Loan extends AbstractPersistableCustom { compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), - holidayDetailDTO, allowCompoundingOnEod); + holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday()); return loanApplicationTerms; } @@ -5764,7 +5764,7 @@ public class Loan extends AbstractPersistableCustom { this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, - calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod); + calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false); } /** 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 5457357a2..612739d4d 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 @@ -29,12 +29,15 @@ import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.holiday.service.HolidayUtil; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO; import org.apache.fineract.organisation.workingdays.domain.RepaymentRescheduleType; +import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -2341,6 +2344,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener periods.clear(); } LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loan.charges(), holidayDetailDTO, loanScheduleParams); + // check if first installment after reschedule is a holiday + if (!loanApplicationTerms.isFirstRepaymentDateAllowedOnHoliday()) { + checkIfFirstRepaymentDateisOnHolidayOrNonWorkingDay(holidayDetailDTO, loanScheduleModel); + } + for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) { if (loanScheduleModelPeriod.isRepaymentPeriod()) { // adding newly created repayment periods to installments @@ -2352,6 +2360,24 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return LoanScheduleDTO.from(retainedInstallments, loanScheduleModelwithPeriodChanges); } + private void checkIfFirstRepaymentDateisOnHolidayOrNonWorkingDay(HolidayDetailDTO holidayDetailDTO, + LoanScheduleModel loanScheduleModel) { + + if (loanScheduleModel != null && loanScheduleModel.getPeriods() != null && loanScheduleModel.getPeriods().iterator() != null + && loanScheduleModel.getPeriods().iterator().next() != null + && loanScheduleModel.getPeriods().iterator().next().periodDueDate() != null) { + + if (WorkingDaysUtil.isNonWorkingDay(holidayDetailDTO.getWorkingDays(), + loanScheduleModel.getPeriods().iterator().next().periodDueDate()) + || HolidayUtil.getApplicableHoliday(loanScheduleModel.getPeriods().iterator().next().periodDueDate(), + holidayDetailDTO.getHolidays()) != null) { + throw new GeneralPlatformDomainRuleException("error.msg.first.installment.date.after.reschedule.should.be.a.working.day", + "As per configiration, the first repayment date after reschedule should be a working day"); + } + } + + } + public List fetchRetainedInstallments( final List repaymentScheduleInstallments, final LocalDate rescheduleFrom, MonetaryCurrency currency) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index 09ee6821f..0ca5e9b83 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -62,6 +62,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { LocalDate dueRepaymentPeriodDate = null; if (isFirstRepayment && firstRepaymentPeriodDate != null) { dueRepaymentPeriodDate = firstRepaymentPeriodDate; + } else { LocalDate seedDate = null; String reccuringString = null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index f1d8fff91..8947018f3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -190,6 +190,8 @@ public final class LoanApplicationTerms { private final boolean isSkipRepaymentOnFirstDayOfMonth; + private final boolean isFirstRepaymentDateAllowedOnHoliday; + private final HolidayDetailDTO holidayDetailDTO; private final Set periodNumbersApplicableForPrincipalGrace = new HashSet<>(); @@ -243,7 +245,7 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, - isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization); + isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false); } @@ -269,7 +271,7 @@ public final class LoanApplicationTerms { principalThresholdForLastInstalment, installmentAmountInMultiplesOf, recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy, rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations, - calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod); + calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -286,7 +288,8 @@ public final class LoanApplicationTerms { final LoanRescheduleStrategyMethod rescheduleStrategyMethod, final Calendar loanCalendar, BigDecimal approvedAmount, BigDecimal annualNominalInterestRate, final List loanTermVariations, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays, - final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod) { + final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, + final boolean isFirstRepaymentDateAllowedOnHoliday) { final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -324,7 +327,7 @@ public final class LoanApplicationTerms { compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, - allowCompoundingOnEod, isEqualAmortization); + allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -383,7 +386,7 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, - isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization); + isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false); } @@ -409,7 +412,8 @@ public final class LoanApplicationTerms { applicationTerms.loanCalendar, applicationTerms.approvedPrincipal.getAmount(), loanTermVariations, applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled, applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO, - applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization); + applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization, + applicationTerms.isFirstRepaymentDateAllowedOnHoliday); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -433,7 +437,7 @@ public final class LoanApplicationTerms { BigDecimal approvedAmount, List loanTermVariations, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, - final boolean allowCompoundingOnEod, final boolean isEqualAmortization) { + final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -506,6 +510,7 @@ public final class LoanApplicationTerms { this.totalInterestAccounted = principal.zero(); this.totalPrincipalAccounted = principal.zero(); this.isEqualAmortization = isEqualAmortization; + this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1769,4 +1774,8 @@ public final class LoanApplicationTerms { this.isEqualAmortization = isEqualAmortization; } + public boolean isFirstRepaymentDateAllowedOnHoliday() { + return isFirstRepaymentDateAllowedOnHoliday; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java index 654ec2b58..2c2c89454 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java @@ -140,11 +140,14 @@ public class LoanUtilService { final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = this.configurationDomainService .isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled(); + boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService + .isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); + ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth, - isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled); + isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday); return scheduleGeneratorDTO; } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V360__conf_loan_payday_on_holiday.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V360__conf_loan_payday_on_holiday.sql new file mode 100644 index 000000000..a10d0f00f --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V360__conf_loan_payday_on_holiday.sql @@ -0,0 +1,22 @@ +-- +-- 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. +-- + +INSERT INTO `c_configuration` (`name`, `value`, `date_value`, `enabled`, `is_trap_door`, `description`) +VALUES + ('loan-reschedule-is-first-payday-allowed-on-holiday', 0, NULL, 0, 0, 'If enabled, while loan reschedule the first repayment date can be on a holiday/non working day');