/* * Copyright (c) 2005-2011 Grameen Foundation USA * All rights reserved. * * Licensed 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. * * See also http://www.apache.org/licenses/LICENSE-2.0.html for an * explanation of the license and how it is applied. */ package org.mifos.calendar; import java.util.ArrayList; import java.util.List; import org.joda.time.DateTime; import org.joda.time.Days; import org.mifos.application.holiday.business.Holiday; import org.mifos.application.holiday.business.HolidayBO; import org.mifos.application.holiday.util.helpers.RepaymentRuleTypes; import org.mifos.dto.domain.HolidayDetails; import org.mifos.schedule.ScheduledEvent; public class MoratoriumStrategy implements ListOfDatesAdjustmentStrategy { /** * should be ordered by date ascending to avoid problems with overlapping holidays * TODO: Fix to allow moratorium periods to overlap holidays. THIS IS IMPORTANT. */ private final List<Holiday> upcomingHolidays; private final List<Days> workingDays; private final ScheduledEvent scheduledEvent; public MoratoriumStrategy(final List<Holiday> upcomingHolidays, final List<Days> workingDays, final ScheduledEvent scheduledEvent) { this.upcomingHolidays = upcomingHolidays; this.workingDays = workingDays; this.scheduledEvent = scheduledEvent; } @Override public List<DateTime> adjust (List<DateTime> dates) { if (dates == null) {throw new IllegalArgumentException("dates cannot be null.");} List<DateTime> adjustedDates = null; if (dates.isEmpty()) { adjustedDates = dates; } else { DateTime firstDate = new DateTime(dates.get(0)).toDateMidnight().toDateTime(); if ( isEnclosedByAHolidayWithRepaymentRule(firstDate, RepaymentRuleTypes.REPAYMENT_MORATORIUM) ) { adjustedDates = adjust (shiftSchedulePastMoratorium(dates)); } else if (isEnclosedByAHoliday(firstDate)) { //enclosed by a non-moratorium holiday adjustedDates = makeList(shiftDatePastNonMoratoriumHoliday(firstDate), adjust (rest(dates))); } else { adjustedDates = makeList (firstDate, adjust (rest (dates))); } } return adjustedDates; } private List<DateTime> shiftSchedulePastMoratorium (List<DateTime> dates) { assert (dates != null) && isEnclosedByAHolidayWithRepaymentRule(dates.get(0), RepaymentRuleTypes.REPAYMENT_MORATORIUM); List<DateTime> shiftedDates = dates; do { shiftedDates = shiftByOneScheduledEventRecurrence(shiftedDates); } while (isEnclosedByAHolidayWithRepaymentRule(shiftedDates.get(0), RepaymentRuleTypes.REPAYMENT_MORATORIUM)); return shiftedDates; } private List<DateTime> shiftByOneScheduledEventRecurrence (List<DateTime> dates) { assert dates != null; List<DateTime> pushedOutSchedule = new ArrayList<DateTime>(); for (DateTime date: dates) { pushedOutSchedule.add(WorkingDay.nearestWorkingDayOnOrAfter(scheduledEvent.nextEventDateAfter(date), workingDays)); } return pushedOutSchedule; } /** * Given that the date is in a non-moratorium holiday, shift it past the holiday until either it is no longer * in a holiday or moratorium, or until it no longer moves (e.g., lands in a same-day holiday). * * <p> If the date shifts into a moratorium period, then shift it out using the RepaymentRuleType of * the most recent non-moratorium holiday that the date was shifted out of. For example, if shifting * the date out of a next-working-day holiday lands it in a moratorium period, then use the * next-working-day repayment rule to shift it past the moratorium period.</p> * * @param date the DateTime to be shifted * @return the shifted date */ private DateTime shiftDatePastNonMoratoriumHoliday (DateTime date) { assert date != null; assert isEnclosedByAHoliday(date); assert ! isEnclosedByAHolidayWithRepaymentRule(date, RepaymentRuleTypes.REPAYMENT_MORATORIUM); Holiday currentlyEnclosingHoliday = getHolidayEnclosing(date); RepaymentRuleTypes mostRecentNonMoratoriumRepaymentRule = currentlyEnclosingHoliday.getRepaymentRuleType(); //never REPAYMENT_MORATORIUM DateTime previousDate = null; DateTime adjustedDate = date; do { previousDate = adjustedDate; if (currentlyEnclosingHoliday.getRepaymentRuleType() == RepaymentRuleTypes.REPAYMENT_MORATORIUM) { adjustedDate = buildHolidayFromCurrentHolidayWithRepaymentRule (currentlyEnclosingHoliday, mostRecentNonMoratoriumRepaymentRule) .adjust(previousDate, workingDays, scheduledEvent); } else { adjustedDate = (new BasicWorkingDayStrategy(workingDays)) .adjust(currentlyEnclosingHoliday.adjust(previousDate, workingDays, scheduledEvent)); mostRecentNonMoratoriumRepaymentRule = currentlyEnclosingHoliday.getRepaymentRuleType(); } if (isEnclosedByAHoliday(adjustedDate)) { currentlyEnclosingHoliday = getHolidayEnclosing(adjustedDate); } } while (isEnclosedByAHoliday(adjustedDate) && (! adjustedDate.equals(previousDate))); return adjustedDate; } private Holiday buildHolidayFromCurrentHolidayWithRepaymentRule(Holiday originalHoliday, RepaymentRuleTypes rule) { HolidayDetails holidayDetails = new HolidayDetails("temporaryHoliday", originalHoliday.getFromDate().toDate(), originalHoliday.getThruDate().toDate(), rule.getValue()); return new HolidayBO(holidayDetails); } private boolean isEnclosedByAHolidayWithRepaymentRule (DateTime date, RepaymentRuleTypes rule) { for (Holiday holiday : this.upcomingHolidays) { if (holiday.encloses(date.toDate()) && (holiday.getRepaymentRuleType() == rule)) { return true; } } return false; } private boolean isEnclosedByAHoliday (DateTime date) { for (Holiday holiday : this.upcomingHolidays) { if (holiday.encloses(date.toDate())) { return true; } } return false; } private Holiday getHolidayEnclosing (DateTime date) { assert isEnclosedByAHoliday(date); Holiday holidayEnclosingDate = null; for (Holiday holiday : upcomingHolidays) { if (holiday.encloses(date.toDate())) { holidayEnclosingDate = holiday; } } return holidayEnclosingDate; } /* * TODO KeithP: These methods should be generic and moved to a ListUtils class */ private List<DateTime> rest (List<DateTime> dates) { assert dates != null; List<DateTime> rest = new ArrayList<DateTime>(); for (int i = 1; i < dates.size(); i++) { rest.add(dates.get(i)); } return rest; } private List<DateTime> makeList (DateTime first, List<DateTime> rest) { assert first != null; assert rest != null; List<DateTime> newList = rest; newList.add(0, first); return newList; } }