blob: 1b4f568b5343798aedc2f809da89c88816c4ec35 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.calendar.service;
import com.google.gson.JsonElement;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.NumberList;
import net.fortuna.ical4j.model.Recur;
import net.fortuna.ical4j.model.WeekDay;
import net.fortuna.ical4j.model.WeekDayList;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType;
import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType;
import org.apache.fineract.portfolio.common.domain.NthDayType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
@Slf4j
public final class CalendarUtils {
public static final String FLOATING_TIMEZONE_PROPERTY_KEY = "net.fortuna.ical4j.timezone.date.floating";
private CalendarUtils() {
}
static {
System.setProperty(FLOATING_TIMEZONE_PROPERTY_KEY, "true");
}
public static LocalDateTime getNextRecurringDate(final String recurringRule, final LocalDateTime seedDate,
final LocalDateTime startDate) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return null;
}
LocalDateTime nextDate = getNextRecurringDate(recur, seedDate, startDate);
nextDate = (LocalDateTime) adjustDate(nextDate, seedDate, getMeetingPeriodFrequencyType(recurringRule));
return nextDate;
}
public static LocalDate getNextRecurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate startDate) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return null;
}
LocalDate nextDate = getNextRecurringDate(recur, seedDate, startDate);
nextDate = (LocalDate) adjustDate(nextDate, seedDate, getMeetingPeriodFrequencyType(recurringRule));
return nextDate;
}
public static Temporal adjustDate(final Temporal date, final Temporal seedDate, final PeriodFrequencyType frequencyType) {
if (frequencyType.isMonthly() && seedDate.get(ChronoField.DAY_OF_MONTH) > 28 && date.get(ChronoField.DAY_OF_MONTH) >= 28) {
int noOfDaysInCurrentMonth = YearMonth.from(date).lengthOfMonth();
int seedDay = seedDate.get(ChronoField.DAY_OF_MONTH);
int adjustedDay = Math.min(noOfDaysInCurrentMonth, seedDay);
return date.with(ChronoField.DAY_OF_MONTH, adjustedDay);
}
return date;
}
private static LocalDateTime getNextRecurringDate(final Recur recur, final LocalDateTime seedDate, final LocalDateTime startDate) {
final DateTime periodStart = new DateTime(java.util.Date.from(startDate.atZone(ZoneId.systemDefault()).toInstant()));
final Date seed = convertToiCal4JCompatibleDate(seedDate);
final Date nextRecDate = recur.getNextDate(seed, periodStart);
return nextRecDate == null ? null : LocalDateTime.ofInstant(nextRecDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
private static LocalDate getNextRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate startDate) {
final DateTime periodStart = new DateTime(java.util.Date.from(startDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
final Date seed = convertToiCal4JCompatibleDate(seedDate.atStartOfDay());
final Date nextRecDate = recur.getNextDate(seed, periodStart);
return nextRecDate == null ? null : LocalDate.ofInstant(nextRecDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
}
private static Date convertToiCal4JCompatibleDate(final LocalDateTime inputDate) {
Date formattedDate = null;
final String seedDateStr = DateUtils.DEFAULT_DATETIME_FORMATTER.format(inputDate);
try {
formattedDate = new Date(seedDateStr, DateUtils.DEFAULT_DATETIME_FORMAT);
} catch (final ParseException e) {
log.error("Invalid date: {}", seedDateStr, e);
}
return formattedDate;
}
public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate, final LocalDate endDate) {
final LocalDate periodStartDate = DateUtils.getLocalDateOfTenant();
final LocalDate periodEndDate = endDate == null ? periodStartDate.plusYears(5) : endDate;
return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate);
}
public static Collection<LocalDate> getRecurringDatesFrom(final String recurringRule, final LocalDate seedDate,
final LocalDate startDate) {
LocalDate currentDate = DateUtils.getLocalDateOfTenant();
final LocalDate periodStartDate = startDate == null ? currentDate : startDate;
final LocalDate periodEndDate = currentDate.plusYears(5);
return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate);
}
public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate,
final LocalDate periodStartDate, final LocalDate periodEndDate) {
final int maxCount = 10;// Default number of recurring dates
boolean isSkipRepaymentOnFirstdayofMonth = false;
final Integer numberofDays = 0;
return getRecurringDates(recurringRule, seedDate, periodStartDate, periodEndDate, maxCount, isSkipRepaymentOnFirstdayofMonth,
numberofDays);
}
public static Collection<LocalDate> getRecurringDates(final String recurringRule, final LocalDate seedDate,
final LocalDate periodStartDate, final LocalDate periodEndDate, final int maxCount, boolean isSkippMeetingOnFirstDay,
final Integer numberOfDays) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
return getRecurringDates(recur, seedDate, periodStartDate, periodEndDate, maxCount, isSkippMeetingOnFirstDay, numberOfDays);
}
private static Collection<LocalDate> getRecurringDates(final Recur recur, final LocalDate seedDate, final LocalDate periodStartDate,
final LocalDate periodEndDate, final int maxCount, boolean isSkippMeetingOnFirstDay, final Integer numberOfDays) {
if (recur == null) {
return null;
}
final Date seed = convertToiCal4JCompatibleDate(seedDate.atStartOfDay());
final DateTime periodStart = new DateTime(java.util.Date.from(periodStartDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
final DateTime periodEnd = new DateTime(java.util.Date.from(periodEndDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
final Value value = new Value(Value.DATE.getValue());
final DateList recurringDates = recur.getDates(seed, periodStart, periodEnd, value, maxCount);
return convertToLocalDateList(recurringDates, seedDate, getMeetingPeriodFrequencyType(recur), isSkippMeetingOnFirstDay,
numberOfDays);
}
static Collection<LocalDate> convertToLocalDateList(final DateList dates, final LocalDate seedDate,
final PeriodFrequencyType frequencyType, boolean isSkippMeetingOnFirstDay, final Integer numberOfDays) {
final Collection<LocalDate> recurringDates = new ArrayList<>();
for (final Date date : dates) {
LocalDateTime dateTimeInProperTz = getLocalDateTimeFromICal4JDate(date);
ZoneId tenantZoneId = DateUtils.getDateTimeZoneOfTenant();
recurringDates.add((LocalDate) adjustDate(dateTimeInProperTz.atZone(tenantZoneId).toLocalDate(), seedDate, frequencyType));
}
if (isSkippMeetingOnFirstDay) {
return skipMeetingOnFirstdayOfMonth(recurringDates, numberOfDays);
}
return recurringDates;
}
private static LocalDateTime getLocalDateTimeFromICal4JDate(Date date) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.setTime(date);
return LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
}
private static Collection<LocalDate> skipMeetingOnFirstdayOfMonth(final Collection<LocalDate> recurringDates,
final Integer numberOfDays) {
final Collection<LocalDate> adjustedRecurringDates = new ArrayList<>();
for (@SuppressWarnings("rawtypes")
final Iterator iterator = recurringDates.iterator(); iterator.hasNext();) {
LocalDate recuringDate = (LocalDate) iterator.next();
adjustedRecurringDates.add(adjustRecurringDate(recuringDate, numberOfDays));
}
return adjustedRecurringDates;
}
public static LocalDate adjustRecurringDate(final LocalDate recuringDate, final Integer numberOfDays) {
if (recuringDate.getDayOfMonth() == 1) {
LocalDate adjustedRecurringDate = recuringDate.plusDays(numberOfDays);
return adjustedRecurringDate;
}
return recuringDate;
}
public static Recur getICalRecur(final String recurringRule) {
// Construct RRule
try {
final RRule rrule = new RRule(recurringRule);
rrule.validate();
final Recur recur = rrule.getRecur();
return recur;
} catch (final ParseException e) {
// TODO Auto-generated catch block
log.error("Problem occurred in getICalRecur function", e);
} catch (final ValidationException e) {
// TODO Auto-generated catch block
log.error("Problem occurred in getICalRecur function", e);
}
return null;
}
public static String getRRuleReadable(final LocalDate startDate, final String recurringRule) {
String humanReadable = "";
RRule rrule;
Recur recur = null;
try {
rrule = new RRule(recurringRule);
rrule.validate();
recur = rrule.getRecur();
} catch (final ValidationException e) {
throw new PlatformDataIntegrityException("error.msg.invalid.recurring.rule",
"The Recurring Rule value: " + recurringRule + " is not valid.", "recurrence", recurringRule, e);
} catch (final ParseException e) {
throw new PlatformDataIntegrityException("error.msg.recurring.rule.parsing.error",
"Error in pasring the Recurring Rule value: " + recurringRule, "recurrence", recurringRule, e);
}
if (recur == null) {
return humanReadable;
}
if (recur.getFrequency().equals(Recur.Frequency.DAILY)) {
if (recur.getInterval() == 1) {
humanReadable = "Daily";
} else {
humanReadable = "Every " + recur.getInterval() + " days";
}
} else if (recur.getFrequency().equals(Recur.Frequency.WEEKLY)) {
if (recur.getInterval() == 1 || recur.getInterval() == -1) {
humanReadable = "Weekly";
} else {
humanReadable = "Every " + recur.getInterval() + " weeks";
}
humanReadable += " on ";
final WeekDayList weekDayList = recur.getDayList();
StringBuilder sb = new StringBuilder();
for (@SuppressWarnings("rawtypes")
final Iterator iterator = weekDayList.iterator(); iterator.hasNext();) {
final WeekDay weekDay = (WeekDay) iterator.next();
sb.append(DayNameEnum.from(weekDay.getDay().name()).getCode());
}
humanReadable += sb.toString();
} else if (recur.getFrequency().equals(Recur.Frequency.MONTHLY)) {
NumberList nthDays = recur.getSetPosList();
Integer nthDay = null;
if (!nthDays.isEmpty()) {
nthDay = nthDays.get(0);
}
NumberList monthDays = recur.getMonthDayList();
Integer monthDay = null;
if (!monthDays.isEmpty()) {
monthDay = monthDays.get(0);
}
WeekDayList weekdays = recur.getDayList();
WeekDay weekDay = null;
if (!weekdays.isEmpty()) {
weekDay = weekdays.get(0);
}
if (nthDay != null && weekDay != null) {
NthDayType nthDayType = NthDayType.fromInt(nthDay);
NthDayNameEnum nthDayName = NthDayNameEnum.from(nthDayType.toString());
DayNameEnum weekdayType = DayNameEnum.from(weekDay.getDay().name());
if (recur.getInterval() == 1 || recur.getInterval() == -1) {
humanReadable = "Monthly on " + nthDayName.getCode().toLowerCase() + " " + weekdayType.getCode().toLowerCase();
} else {
humanReadable = "Every " + recur.getInterval() + " months on " + nthDayName.getCode().toLowerCase() + " "
+ weekdayType.getCode().toLowerCase();
}
} else if (monthDay != null) {
if (monthDay == -1) {
if (recur.getInterval() == 1 || recur.getInterval() == -1) {
humanReadable = "Monthly on last day";
} else {
humanReadable = "Every " + recur.getInterval() + " months on last day";
}
} else {
if (recur.getInterval() == 1 || recur.getInterval() == -1) {
humanReadable = "Monthly on day " + monthDay;
} else {
humanReadable = "Every " + recur.getInterval() + " months on day " + monthDay;
}
}
} else {
if (recur.getInterval() == 1 || recur.getInterval() == -1) {
humanReadable = "Monthly on day " + startDate.getDayOfMonth();
} else {
humanReadable = "Every " + recur.getInterval() + " months on day " + startDate.getDayOfMonth();
}
}
} else if (recur.getFrequency().equals(Recur.Frequency.YEARLY)) {
if (recur.getInterval() == 1) {
humanReadable = "Annually on " + startDate.format(DateTimeFormatter.ofPattern("MMM")) + " " + startDate.getDayOfMonth();
} else {
humanReadable = "Every " + recur.getInterval() + " years on " + startDate.format(DateTimeFormatter.ofPattern("MMM")) + " "
+ startDate.getDayOfMonth();
}
}
if (recur.getCount() > 0) {
if (recur.getCount() == 1) {
humanReadable = "Once";
}
humanReadable += ", " + recur.getCount() + " times";
}
final Date endDate = recur.getUntil();
if (endDate != null) {
final LocalDate date = LocalDate.ofInstant(endDate.toInstant(), DateUtils.getDateTimeZoneOfTenant());
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMMM yy");
final String formattedDate = date.format(fmt);
humanReadable += ", until " + formattedDate;
}
return humanReadable;
}
public static boolean isValidRedurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate date) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return false;
}
final boolean isSkipRepaymentonFirstDayOfMonth = false;
final int numberOfDays = 0;
return isValidRecurringDate(recur, seedDate, date, isSkipRepaymentonFirstDayOfMonth, numberOfDays);
}
public static boolean isValidRedurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate date,
boolean isSkipRepaymentonFirstDayOfMonth, final Integer numberOfDays) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return false;
}
return isValidRecurringDate(recur, seedDate, date, isSkipRepaymentonFirstDayOfMonth, numberOfDays);
}
public static boolean isValidRecurringDate(final Recur recur, final LocalDate seedDate, final LocalDate date,
boolean isSkipRepaymentonFirstDayOfMonth, final int numberOfDays) {
LocalDate startDate = date;
if (isSkipRepaymentonFirstDayOfMonth && date.getDayOfMonth() == (numberOfDays + 1)) {
startDate = startDate.minusDays(numberOfDays);
}
final Collection<LocalDate> recurDate = getRecurringDates(recur, seedDate, startDate, date.plusDays(1), 1,
isSkipRepaymentonFirstDayOfMonth, numberOfDays);
return (recurDate == null || recurDate.isEmpty()) ? false : recurDate.contains(date);
}
public enum DayNameEnum {
MO(1, "Monday"), TU(2, "Tuesday"), WE(3, "Wednesday"), TH(4, "Thursday"), FR(5, "Friday"), SA(6, "Saturday"), SU(7, "Sunday");
private final String code;
private final Integer value;
DayNameEnum(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public String getCode() {
return this.code;
}
public int getValue() {
return this.value;
}
public static DayNameEnum from(final String name) {
for (final DayNameEnum dayName : DayNameEnum.values()) {
if (dayName.toString().equals(name)) {
return dayName;
}
}
return DayNameEnum.MO;// Default it to Monday
}
}
public enum NthDayNameEnum {
ONE(1, "First"), TWO(2, "Second"), THREE(3, "Third"), FOUR(4, "Fourth"), FIVE(5, "Fifth"), LAST(-1, "Last"), INVALID(0, "Invalid");
private final String code;
private final Integer value;
NthDayNameEnum(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public String getCode() {
return this.code;
}
public int getValue() {
return this.value;
}
public static NthDayNameEnum from(final String name) {
for (final NthDayNameEnum nthDayName : NthDayNameEnum.values()) {
if (nthDayName.toString().equals(name)) {
return nthDayName;
}
}
return NthDayNameEnum.INVALID;
}
}
public static PeriodFrequencyType getMeetingPeriodFrequencyType(final String recurringRule) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
return getMeetingPeriodFrequencyType(recur);
}
private static PeriodFrequencyType getMeetingPeriodFrequencyType(final Recur recur) {
PeriodFrequencyType meetingFrequencyType = PeriodFrequencyType.INVALID;
if (recur.getFrequency().equals(Recur.Frequency.DAILY)) {
meetingFrequencyType = PeriodFrequencyType.DAYS;
} else if (recur.getFrequency().equals(Recur.Frequency.WEEKLY)) {
meetingFrequencyType = PeriodFrequencyType.WEEKS;
} else if (recur.getFrequency().equals(Recur.Frequency.MONTHLY)) {
meetingFrequencyType = PeriodFrequencyType.MONTHS;
} else if (recur.getFrequency().equals(Recur.Frequency.YEARLY)) {
meetingFrequencyType = PeriodFrequencyType.YEARS;
}
return meetingFrequencyType;
}
public static String getMeetingFrequencyFromPeriodFrequencyType(final PeriodFrequencyType periodFrequency) {
String frequency = null;
if (periodFrequency.equals(PeriodFrequencyType.DAYS)) {
frequency = Recur.Frequency.DAILY.name();
} else if (periodFrequency.equals(PeriodFrequencyType.WEEKS)) {
frequency = Recur.Frequency.WEEKLY.name();
} else if (periodFrequency.equals(PeriodFrequencyType.MONTHS)) {
frequency = Recur.Frequency.MONTHLY.name();
} else if (periodFrequency.equals(PeriodFrequencyType.YEARS)) {
frequency = Recur.Frequency.YEARLY.name();
}
return frequency;
}
public static int getInterval(final String recurringRule) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
return recur.getInterval();
}
public static CalendarFrequencyType getFrequency(final String recurringRule) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
return CalendarFrequencyType.fromString(recur.getFrequency().name());
}
public static CalendarWeekDaysType getRepeatsOnDay(final String recurringRule) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
final WeekDayList weekDays = recur.getDayList();
if (weekDays.isEmpty()) {
return CalendarWeekDaysType.INVALID;
}
// supports only one day
WeekDay weekDay = weekDays.get(0);
return CalendarWeekDaysType.fromString(weekDay.getDay().name());
}
public static NthDayType getRepeatsOnNthDayOfMonth(final String recurringRule) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
NumberList monthDays = null;
if (recur.getDayList().isEmpty()) {
monthDays = recur.getMonthDayList();
} else {
monthDays = recur.getSetPosList();
}
if (monthDays.isEmpty()) {
return NthDayType.INVALID;
}
if (!recur.getMonthDayList().isEmpty() && recur.getSetPosList().isEmpty()) {
return NthDayType.ONDAY;
}
Integer monthDay = monthDays.get(0);
return NthDayType.fromInt(monthDay);
}
public static LocalDate getFirstRepaymentMeetingDate(final Calendar calendar, final LocalDate disbursementDate,
final Integer loanRepaymentInterval, final String frequency, boolean isSkipRepaymentOnFirstDayOfMonth,
final Integer numberOfDays) {
final Recur recur = CalendarUtils.getICalRecur(calendar.getRecurrence());
if (recur == null) {
return null;
}
LocalDate startDate = disbursementDate;
final LocalDate seedDate = calendar.getStartDateLocalDate();
if (isValidRedurringDate(calendar.getRecurrence(), seedDate, startDate, isSkipRepaymentOnFirstDayOfMonth, numberOfDays)
&& !frequency.equals(Recur.Frequency.DAILY.name())) {
startDate = startDate.plusDays(1);
}
// Recurring dates should follow loanRepaymentInterval.
// e.g.
// for weekly meeting interval is 1
// where as for loan product with fortnightly frequency interval is 2
// to generate currect set of meeting dates reset interval same as loan
// repayment interval.
Recur.Builder recurBuilder = getRecurBuilder(recur);
recurBuilder = recurBuilder.interval(loanRepaymentInterval);
// Recurring dates should follow loanRepayment frequency.
// e.g.
// daily meeting frequency should support all loan products with any
// frequency type.
// to generate currect set of meeting dates reset frequency same as loan
// repayment frequency.
if (recur.getFrequency().equals(Recur.Frequency.DAILY)) {
recurBuilder = recurBuilder.frequency(Recur.Frequency.valueOf(frequency));
}
Recur modifiedRecur = recurBuilder.build();
final LocalDate firstRepaymentDate = getNextRecurringDate(modifiedRecur, seedDate, startDate);
if (isSkipRepaymentOnFirstDayOfMonth && firstRepaymentDate.getDayOfMonth() == 1) {
return adjustRecurringDate(firstRepaymentDate, numberOfDays);
}
return firstRepaymentDate;
}
public static LocalDate getNewRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate,
final LocalDate oldRepaymentDate, final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays,
final boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return null;
}
if (isValidRecurringDate(recur, seedDate, oldRepaymentDate, isSkipRepaymentOnFirstDayOfMonth, numberOfDays)) {
return oldRepaymentDate;
}
LocalDate nextRepaymentDate = getNextRepaymentMeetingDate(recurringRule, seedDate, oldRepaymentDate, loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays);
return nextRepaymentDate;
}
public static LocalDate getNextRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate repaymentDate,
final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays,
boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return null;
}
LocalDate tmpDate = repaymentDate;
if (isValidRecurringDate(recur, seedDate, repaymentDate, isSkipRepaymentOnFirstDayOfMonth, numberOfDays)) {
tmpDate = repaymentDate.plusDays(1);
}
/*
* Recurring dates should follow loanRepaymentInterval.
*
* e.g. The weekly meeting will have interval of 1, if the loan product with fortnightly frequency will have
* interval of 2, to generate right set of meeting dates reset interval same as loan repayment interval.
*/
Recur.Builder recurBuilder = getRecurBuilder(recur);
recurBuilder = recurBuilder.interval(loanRepaymentInterval);
/*
* Recurring dates should follow loanRepayment frequency. //e.g. daily meeting frequency should support all loan
* products with any type of frequency. to generate right set of meeting dates reset frequency same as loan
* repayment frequency.
*/
if (recur.getFrequency().equals(Recur.Frequency.DAILY)) {
recurBuilder = recurBuilder.frequency(Recur.Frequency.valueOf(frequency));
}
Recur modifiedRecur = recurBuilder.build();
LocalDate newRepaymentDate = getNextRecurringDate(modifiedRecur, seedDate, tmpDate);
final LocalDate nextRepaymentDate = getNextRecurringDate(modifiedRecur, seedDate, newRepaymentDate);
newRepaymentDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(newRepaymentDate, nextRepaymentDate, workingDays);
if (isSkipRepaymentOnFirstDayOfMonth) {
LocalDate newRepaymentDateTemp = adjustRecurringDate(newRepaymentDate, numberOfDays);
return WorkingDaysUtil.getOffSetDateIfNonWorkingDay(newRepaymentDateTemp, nextRepaymentDate, workingDays);
}
return newRepaymentDate;
}
public static boolean isFrequencySame(final String oldRRule, final String newRRule) {
final Recur oldRecur = getICalRecur(oldRRule);
final Recur newRecur = getICalRecur(newRRule);
if (oldRecur == null || oldRecur.getFrequency() == null || newRecur == null || newRecur.getFrequency() == null) {
return false;
}
return oldRecur.getFrequency().equals(newRecur.getFrequency());
}
public static boolean isIntervalSame(final String oldRRule, final String newRRule) {
final Recur oldRecur = getICalRecur(oldRRule);
final Recur newRecur = getICalRecur(newRRule);
if (oldRecur == null || oldRecur.getFrequency() == null || newRecur == null || newRecur.getFrequency() == null) {
return false;
}
return (oldRecur.getInterval() == newRecur.getInterval());
}
public static List<Integer> createIntegerListFromQueryParameter(final String calendarTypeQuery) {
final List<Integer> calendarTypeOptions = new ArrayList<>();
// adding all calendar Types if query parameter is "all"
if (calendarTypeQuery.equalsIgnoreCase("all")) {
calendarTypeOptions.add(1);
calendarTypeOptions.add(2);
calendarTypeOptions.add(3);
calendarTypeOptions.add(4);
return calendarTypeOptions;
}
// creating a list of calendar type options from the comma separated
// query parameter.
final List<String> calendarTypeOptionsInQuery = new ArrayList<>();
final StringTokenizer st = new StringTokenizer(calendarTypeQuery, ",");
while (st.hasMoreElements()) {
calendarTypeOptionsInQuery.add(st.nextElement().toString());
}
for (final String calType : calendarTypeOptionsInQuery) {
if (calType.equalsIgnoreCase("collection")) {
calendarTypeOptions.add(1);
} else if (calType.equalsIgnoreCase("training")) {
calendarTypeOptions.add(2);
} else if (calType.equalsIgnoreCase("audit")) {
calendarTypeOptions.add(3);
} else if (calType.equalsIgnoreCase("general")) {
calendarTypeOptions.add(4);
}
}
return calendarTypeOptions;
}
/**
* function returns a comma separated list of calendar_type_enum values ex. 1,2,3,4
*
* @param calendarTypeOptions
* @return
*/
public static String getSqlCalendarTypeOptionsInString(final List<Integer> calendarTypeOptions) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < calendarTypeOptions.size() - 1; i++) {
sb.append(calendarTypeOptions.get(i).toString() + ",");
}
sb.append(calendarTypeOptions.get(calendarTypeOptions.size() - 1).toString());
return sb.toString();
}
public static LocalDate getRecentEligibleMeetingDate(final String recurringRule, final LocalDate seedDate,
final boolean isSkipMeetingOnFirstDay, final Integer numberOfDays) {
LocalDate currentDate = DateUtils.getLocalDateOfTenant();
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return null;
}
if (isValidRecurringDate(recur, seedDate, currentDate, isSkipMeetingOnFirstDay, numberOfDays)) {
return currentDate;
}
if (recur.getFrequency().equals(Recur.Frequency.DAILY)) {
currentDate = currentDate.plusDays(recur.getInterval());
} else if (recur.getFrequency().equals(Recur.Frequency.WEEKLY)) {
currentDate = currentDate.plusWeeks(recur.getInterval());
} else if (recur.getFrequency().equals(Recur.Frequency.MONTHLY)) {
currentDate = currentDate.plusMonths(recur.getInterval());
} else if (recur.getFrequency().equals(Recur.Frequency.YEARLY)) {
currentDate = currentDate.plusYears(recur.getInterval());
}
return getNextRecurringDate(recur, seedDate, currentDate);
}
public static LocalDate getNextScheduleDate(final Calendar calendar, final LocalDate startDate) {
final Recur recur = CalendarUtils.getICalRecur(calendar.getRecurrence());
if (recur == null) {
return null;
}
final LocalDate seedDate = calendar.getStartDateLocalDate();
/**
* if (isValidRedurringDate(calendar.getRecurrence(), seedDate, date)) { date = date.plusDays(1); }
**/
return getNextRecurringDate(recur, seedDate, startDate);
}
public static void validateNthDayOfMonthFrequency(DataValidatorBuilder baseDataValidator, final String repeatsOnNthDayOfMonthParamName,
final String repeatsOnDayParamName, final JsonElement element, final FromJsonHelper fromApiJsonHelper) {
final Integer repeatsOnNthDayOfMonth = fromApiJsonHelper.extractIntegerSansLocaleNamed(repeatsOnNthDayOfMonthParamName, element);
baseDataValidator.reset().parameter(repeatsOnNthDayOfMonthParamName).value(repeatsOnNthDayOfMonth).ignoreIfNull()
.isOneOfTheseValues(NthDayType.ONE.getValue(), NthDayType.TWO.getValue(), NthDayType.THREE.getValue(),
NthDayType.FOUR.getValue(), NthDayType.LAST.getValue(), NthDayType.ONDAY.getValue());
final Integer repeatsOnDay = fromApiJsonHelper.extractIntegerSansLocaleNamed(repeatsOnDayParamName, element);
baseDataValidator.reset().parameter(repeatsOnDayParamName).value(repeatsOnDay).ignoreIfNull()
.inMinMaxRange(CalendarWeekDaysType.getMinValue(), CalendarWeekDaysType.getMaxValue());
NthDayType nthDayType = null;
if (repeatsOnNthDayOfMonth != null) {
nthDayType = NthDayType.fromInt(repeatsOnNthDayOfMonth);
}
if (nthDayType != null && nthDayType != NthDayType.INVALID) {
if (nthDayType == NthDayType.ONE || nthDayType == NthDayType.TWO || nthDayType == NthDayType.THREE
|| nthDayType == NthDayType.FOUR) {
baseDataValidator.reset().parameter(repeatsOnDayParamName).value(repeatsOnDay).cantBeBlankWhenParameterProvidedIs(
repeatsOnNthDayOfMonthParamName, NthDayNameEnum.from(nthDayType.toString()).getCode().toLowerCase());
}
}
}
public static Integer getMonthOnDay(String recurringRule) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
NumberList monthDayList = null;
Integer monthOnDay = null;
if (getMeetingPeriodFrequencyType(recur).isMonthly()) {
monthDayList = recur.getMonthDayList();
if (!monthDayList.isEmpty()) {
monthOnDay = monthDayList.get(0);
}
}
return monthOnDay;
}
public static LocalDate getNextRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate repaymentDate,
final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays,
boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays, boolean applyWorkingDays) {
boolean isCalledFirstTime = true;
return getNextRepaymentMeetingDate(recurringRule, seedDate, repaymentDate, loanRepaymentInterval, frequency, workingDays,
isSkipRepaymentOnFirstDayOfMonth, numberOfDays, isCalledFirstTime, applyWorkingDays);
}
public static LocalDate getNextRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate repaymentDate,
final Integer loanRepaymentInterval, final String frequency, boolean isSkipRepaymentOnFirstDayOfMonth,
final Integer numberOfDays) {
boolean isCalledFirstTime = true;
final WorkingDays workingDays = null;
boolean applyWorkingDays = false;
return getNextRepaymentMeetingDate(recurringRule, seedDate, repaymentDate, loanRepaymentInterval, frequency, workingDays,
isSkipRepaymentOnFirstDayOfMonth, numberOfDays, isCalledFirstTime, applyWorkingDays);
}
public static LocalDate getNextRepaymentMeetingDate(final String recurringRule, final LocalDate seedDate, final LocalDate repaymentDate,
final Integer loanRepaymentInterval, final String frequency, final WorkingDays workingDays,
boolean isSkipRepaymentOnFirstDayOfMonth, final Integer numberOfDays, boolean isCalledFirstTime, boolean applyWorkingDays) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return null;
}
LocalDate tmpDate = repaymentDate;
final Integer repaymentInterval = getMeetingIntervalFromFrequency(loanRepaymentInterval, frequency, recur);
/*
* Recurring dates should follow loanRepaymentInterval.
*
* e.g. The weekly meeting will have interval of 1, if the loan product with fortnightly frequency will have
* interval of 2, to generate right set of meeting dates reset interval same as loan repayment interval.
*/
int meetingInterval = recur.getInterval();
if (meetingInterval < 1) {
meetingInterval = 1;
}
int rep = repaymentInterval < meetingInterval ? 1 : repaymentInterval / meetingInterval;
/*
* Recurring dates should follow loanRepayment frequency. //e.g. daily meeting frequency should support all loan
* products with any type of frequency. to generate right set of meeting dates reset frequency same as loan
* repayment frequency.
*/
Recur.Builder recurBuilder = getRecurBuilder(recur);
if (recur.getFrequency().equals(Recur.Frequency.DAILY)) {
recurBuilder = recurBuilder.frequency(Recur.Frequency.valueOf(frequency));
}
Recur modifiedRecur = recurBuilder.build();
/**
* Below code modified as discussed with Pramod N
*/
LocalDate newRepaymentDate = tmpDate;
int newRepayment = rep;
while (newRepayment > 0) {
newRepaymentDate = getNextRecurringDate(modifiedRecur, seedDate, newRepaymentDate);
newRepayment--;
}
LocalDate nextRepaymentDate = null;
if (applyWorkingDays) {
if (WorkingDaysUtil.isNonWorkingDay(workingDays, newRepaymentDate)
&& WorkingDaysUtil.getRepaymentRescheduleType(workingDays).isMoveToNextRepaymentDay()) {
newRepaymentDate = getNextRepaymentMeetingDate(recurringRule, seedDate, newRepaymentDate.plusDays(1), loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays, isCalledFirstTime, applyWorkingDays);
} else {
newRepaymentDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(newRepaymentDate, nextRepaymentDate, workingDays);
}
}
if (isCalledFirstTime && newRepaymentDate.equals(repaymentDate)) {
isCalledFirstTime = false;
newRepaymentDate = getNextRepaymentMeetingDate(recurringRule, seedDate, repaymentDate.plusDays(1), loanRepaymentInterval,
frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays, isCalledFirstTime, applyWorkingDays);
}
if (isSkipRepaymentOnFirstDayOfMonth) {
final LocalDate newRepaymentDateTemp = adjustRecurringDate(newRepaymentDate, numberOfDays);
if (applyWorkingDays) {
if (WorkingDaysUtil.isNonWorkingDay(workingDays, newRepaymentDateTemp)
&& WorkingDaysUtil.getRepaymentRescheduleType(workingDays).isMoveToNextRepaymentDay()) {
newRepaymentDate = getNextRepaymentMeetingDate(recurringRule, seedDate, newRepaymentDate.plusDays(1),
loanRepaymentInterval, frequency, workingDays, isSkipRepaymentOnFirstDayOfMonth, numberOfDays,
isCalledFirstTime, applyWorkingDays);
} else {
newRepaymentDate = WorkingDaysUtil.getOffSetDateIfNonWorkingDay(newRepaymentDateTemp, nextRepaymentDate, workingDays);
}
}
}
return newRepaymentDate;
}
public static Integer getMeetingIntervalFromFrequency(final Integer loanRepaymentInterval, final String frequency, final Recur recur) {
final Integer interval = 4;
Integer repaymentInterval = loanRepaymentInterval;
/*
* check loanRepaymentInterval equal to 1, if repayments frequency is monthly and meeting frequency is weekly,
* then generate repayments schedule as every 4 weeks
*/
if (frequency.equals(Recur.Frequency.MONTHLY.name()) && recur.getFrequency().equals(Recur.Frequency.WEEKLY)) {
repaymentInterval = loanRepaymentInterval * interval;
}
return repaymentInterval;
}
private static Recur.Builder getRecurBuilder(Recur recur) {
Recur.Builder recurBuilder = new Recur.Builder();
recurBuilder = recurBuilder.frequency(recur.getFrequency()).until(recur.getUntil()).count(recur.getCount())
.interval(recur.getInterval()).secondList(recur.getSecondList()).minuteList(recur.getMinuteList())
.hourList(recur.getHourList()).dayList(recur.getDayList()).monthDayList(recur.getMonthDayList())
.yearDayList(recur.getYearDayList()).weekNoList(recur.getWeekNoList()).monthList(recur.getMonthList())
.setPosList(recur.getSetPosList()).weekStartDay(recur.getWeekStartDay());
return recurBuilder;
}
}