diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index d706e8fd514..c8a9ccb6f09 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -352,6 +352,24 @@ public static String format(LocalDateTime dateTime, String format, Locale locale return dateTime == null ? null : dateTime.format(getDateTimeFormatter(format, locale)); } + /** + * Checks if a specific date falls within a given range (inclusive). + * + * @param targetDate + * the date to be checked + * @param startDate + * the start date of the range + * @param endDate + * the end date of the range + * @return true if targetDate is within range or equal to start/end dates, otherwise false + */ + public static boolean isDateWithinRange(LocalDate targetDate, LocalDate startDate, LocalDate endDate) { + if (targetDate == null || startDate == null || endDate == null) { + throw new IllegalArgumentException("Dates must not be null"); + } + return !targetDate.isBefore(startDate) && !targetDate.isAfter(endDate); + } + @NotNull private static DateTimeFormatter getDateFormatter(String format, Locale locale) { DateTimeFormatter formatter = DEFAULT_DATE_FORMATTER; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applyholidaystoloans/ApplyHolidaysToLoansTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applyholidaystoloans/ApplyHolidaysToLoansTasklet.java index cf77e23c62b..67bbeeac657 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applyholidaystoloans/ApplyHolidaysToLoansTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/applyholidaystoloans/ApplyHolidaysToLoansTasklet.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.jobs.applyholidaystoloans; +import static org.apache.fineract.infrastructure.core.service.DateUtils.isDateWithinRange; + import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; @@ -129,13 +131,13 @@ private void adjustRepaymentSchedules(Loan loan, Holiday holiday, LocalDate adju loanRepaymentScheduleInstallment.updateFromDate(tmpFromDate); } - if (!DateUtils.isBefore(oldDueDate, holiday.getFromDate())) { + if (isDateWithinRange(oldDueDate, holiday.getFromDate(), holiday.getToDate())) { // FIXME: AA do we need to apply non-working days. // Assuming holiday's repayment reschedule to date cannot be // created on a non-working day. - adjustedRescheduleToDate = scheduledDateGenerator.generateNextRepaymentDate(adjustedRescheduleToDate, loanApplicationTerms, - false); + adjustedRescheduleToDate = scheduledDateGenerator.generateNextRepaymentDateWhenHolidayApply(adjustedRescheduleToDate, + loanApplicationTerms); loanRepaymentScheduleInstallment.updateDueDate(adjustedRescheduleToDate); } tmpFromDate = loanRepaymentScheduleInstallment.getDueDate(); 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 72e529af060..5108bff37c1 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 @@ -358,4 +358,42 @@ public LocalDate generateNextScheduleDateStartingFromDisburseDateOrRescheduleDat } return adjustedDate; } + + public LocalDate generateNextRepaymentDateWhenHolidayApply(final LocalDate lastRepaymentDate, + final LoanApplicationTerms loanApplicationTerms) { + LocalDate seedDate; + String reccuringString; + Calendar currentCalendar = loanApplicationTerms.getLoanCalendar(); + LocalDate dueRepaymentPeriodDate = lastRepaymentDate; + dueRepaymentPeriodDate = (LocalDate) CalendarUtils.adjustDate(dueRepaymentPeriodDate, loanApplicationTerms.getSeedDate(), + loanApplicationTerms.getRepaymentPeriodFrequencyType()); + if (currentCalendar != null) { + // If we have currentCalendar object, this means there is a + // calendar associated with + // the loan, and we should use it in order to calculate next + // repayment + + CalendarHistory calendarHistory = null; + CalendarHistoryDataWrapper calendarHistoryDataWrapper = loanApplicationTerms.getCalendarHistoryDataWrapper(); + if (calendarHistoryDataWrapper != null) { + calendarHistory = loanApplicationTerms.getCalendarHistoryDataWrapper().getCalendarHistory(dueRepaymentPeriodDate); + } + + // get the start date from the calendar history + if (calendarHistory == null) { + seedDate = currentCalendar.getStartDateLocalDate(); + reccuringString = currentCalendar.getRecurrence(); + } else { + seedDate = calendarHistory.getStartDate(); + reccuringString = calendarHistory.getRecurrence(); + } + + dueRepaymentPeriodDate = CalendarUtils.getNextRepaymentMeetingDate(reccuringString, seedDate, lastRepaymentDate, + loanApplicationTerms.getRepaymentEvery(), + CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(loanApplicationTerms.getLoanTermPeriodFrequencyType()), + loanApplicationTerms.isSkipRepaymentOnFirstDayofMonth(), loanApplicationTerms.getNumberOfdays()); + } + + return dueRepaymentPeriodDate; + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java index bb8d478b736..eb410a05dbd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SchedulerJobsTestResults.java @@ -44,9 +44,11 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.TimeZone; import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; @@ -299,7 +301,7 @@ public void testApplyHolidaysToLoansJobOutcome() throws InterruptedException { final Integer loanProductID = createLoanProduct(null); Assertions.assertNotNull(loanProductID); - final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2013"); + final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "01 January 2013"); Assertions.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); @@ -329,21 +331,49 @@ public void testApplyHolidaysToLoansJobOutcome() throws InterruptedException { if (!enabled) { enabled = true; - configId = GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, configId, enabled); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, configId, enabled); } holidayId = HolidayHelper.activateHolidays(requestSpec, responseSpec, holidayId.toString()); Assertions.assertNotNull(holidayId); + HashMap holidayData = HolidayHelper.getHolidayById(requestSpec, responseSpec, holidayId.toString()); + ArrayList repaymentsRescheduledDate = (ArrayList) holidayData.get("repaymentsRescheduledTo"); + + // Loan Repayment Schedule Before Apply Holidays To Loans + LinkedHashMap repaymentScheduleHashMap = JsonPath.from(loanDetails).get("repaymentSchedule"); + ArrayList periods = (ArrayList) repaymentScheduleHashMap.get("periods"); + + for (LinkedHashMap period : periods) { + ArrayList fromDate = (ArrayList) period.get("fromDate"); + if (fromDate != null && Objects.equals(fromDate.get(1), repaymentsRescheduledDate.get(1))) { + Assertions.assertNotEquals(repaymentsRescheduledDate.get(2), fromDate.get(2), + "Verifying Repayment Rescheduled Day before Running Apply Holidays to Loans Scheduler Job"); + } + } + String JobName = "Apply Holidays To Loans"; this.schedulerJobHelper.executeAndAwaitJob(JobName); - HashMap holidayData = HolidayHelper.getHolidayById(requestSpec, responseSpec, holidayId.toString()); - ArrayList repaymentsRescheduledDate = (ArrayList) holidayData.get("repaymentsRescheduledTo"); + // Loan Repayment Schedule After Apply Holidays To Loans + loanDetails = this.loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + repaymentScheduleHashMap = JsonPath.from(loanDetails).get("repaymentSchedule"); + periods = (ArrayList) repaymentScheduleHashMap.get("periods"); + ArrayList dateToApplyHolidays = null; + + for (LinkedHashMap period : periods) { + ArrayList fromDate = (ArrayList) period.get("fromDate"); + if (fromDate != null && Objects.equals(fromDate.get(1), repaymentsRescheduledDate.get(1))) { + dateToApplyHolidays = fromDate; + } + } - Assertions.assertEquals(repaymentsRescheduledDate, repaymentsRescheduledDate, - "Verifying Repayment Rescheduled Date after Running Apply Holidays to Loans Scheduler Job"); + Assertions.assertNotNull(dateToApplyHolidays); + Assertions.assertEquals(repaymentsRescheduledDate.get(0), dateToApplyHolidays.get(0), + "Verifying Repayment Rescheduled Year after Running Apply Holidays to Loans Scheduler Job"); + Assertions.assertEquals(repaymentsRescheduledDate.get(2), dateToApplyHolidays.get(2), + "Verifying Repayment Rescheduled Day after Running Apply Holidays to Loans Scheduler Job"); } @Test