/*
* 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.application.meeting.business;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.mifos.application.meeting.MeetingTemplate;
import org.mifos.application.meeting.exceptions.MeetingException;
import org.mifos.application.meeting.util.helpers.MeetingConstants;
import org.mifos.application.meeting.util.helpers.MeetingType;
import org.mifos.application.meeting.util.helpers.RankOfDay;
import org.mifos.application.meeting.util.helpers.RecurrenceType;
import org.mifos.application.meeting.util.helpers.WeekDay;
import org.mifos.config.FiscalCalendarRules;
import org.mifos.config.persistence.ConfigurationPersistence;
import org.mifos.dto.domain.MeetingDetailsDto;
import org.mifos.dto.domain.MeetingDto;
import org.mifos.dto.domain.MeetingTypeDto;
import org.mifos.framework.business.AbstractBusinessObject;
import org.mifos.framework.util.helpers.DateUtils;
import org.mifos.schedule.ScheduledEvent;
import org.mifos.schedule.ScheduledEventFactory;
/**
* A better name for MeetingBO would be along the lines of "ScheduledEvent". To
* see what a "meeting" can be look at {@link MeetingType}. It encompasses not
* only a customer meeting, but also financial events like loan installments,
* interest posting and the like. This should be refactored, perhaps from a
* ScheduledEvent base class with subclasses that correspond to the different
* MeetingType entries. In this way a member like meetingPlace could be
* associated with the CustomerMeeting rather than all MeetingTypes.
*/
public class MeetingBO extends AbstractBusinessObject {
private Integer meetingId;
private MeetingDetailsEntity meetingDetails;
private MeetingTypeEntity meetingType;
private Date meetingStartDate;
private String meetingPlace;
private FiscalCalendarRules fiscalCalendarRules = null;
public FiscalCalendarRules getFiscalCalendarRules() {
if (fiscalCalendarRules == null) {
fiscalCalendarRules = new FiscalCalendarRules();
}
return this.fiscalCalendarRules;
}
public void setFiscalCalendarRules(FiscalCalendarRules fiscalCalendarRules) {
this.fiscalCalendarRules = fiscalCalendarRules;
}
/**
* default constructor for hibernate
*/
protected MeetingBO() {
}
/**
* minimal legal constructor
*/
public MeetingBO(final MeetingType meetingType, final Date startDate, final String meetingLocation) {
this.meetingType = new MeetingTypeEntity(meetingType);
this.meetingStartDate = startDate;
this.meetingPlace = meetingLocation;
}
public MeetingBO(final RecurrenceType recurrenceType, final Short recurAfter, final Date startDate, final MeetingType meetingType)
throws MeetingException {
this(recurrenceType, Short.valueOf("1"), WeekDay.MONDAY, null, recurAfter, startDate, meetingType, "meetingPlace");
}
public MeetingBO(final WeekDay weekDay, final RankOfDay rank, final Short recurAfter, final Date startDate, final MeetingType meetingType,
final String meetingPlace) throws MeetingException {
this(weekDay, rank, recurAfter, startDate, meetingType, meetingPlace, null);
}
public MeetingBO(final WeekDay weekDay, final RankOfDay rank, final Short recurAfter, final Date startDate, final MeetingType meetingType,
final String meetingPlace, @SuppressWarnings("unused") final Locale locale) throws MeetingException {
this(RecurrenceType.MONTHLY, null, weekDay, rank, recurAfter, startDate, meetingType, meetingPlace, null);
}
public MeetingBO(final Short dayNumber, final Short recurAfter, final Date startDate, final MeetingType meetingType, final String meetingPlace)
throws MeetingException {
this(RecurrenceType.MONTHLY, dayNumber, null, null, recurAfter, startDate, meetingType, meetingPlace);
}
public MeetingBO(final WeekDay weekDay, final Short recurAfter, final Date startDate, final MeetingType meetingType, final String meetingPlace)
throws MeetingException {
this(RecurrenceType.WEEKLY, null, weekDay, null, recurAfter, startDate, meetingType, meetingPlace);
}
public MeetingBO(final Short recurAfter, Date startDate, MeetingType meetingType, String meetingPlace)
throws MeetingException {
this(RecurrenceType.DAILY, null, null, null, recurAfter, startDate, meetingType, meetingPlace);
}
public MeetingBO(final MeetingTemplate template) throws MeetingException {
this(template.getReccurenceType(), template.getDateNumber(), template.getWeekDay(), template.getRankType(),
template.getRecurAfter(), template.getStartDate(), template.getMeetingType(), template
.getMeetingPlace());
}
private MeetingBO(final RecurrenceType recurrenceType, final Short dayNumber, final WeekDay weekDay, final RankOfDay rank, final Short recurAfter,
final Date startDate, final MeetingType meetingType, final String meetingPlace) throws MeetingException {
this(recurrenceType, dayNumber, weekDay, rank, recurAfter, startDate, meetingType, meetingPlace, null);
}
private MeetingBO(final RecurrenceType recurrenceType, final Short dayNumber, final WeekDay weekDay, final RankOfDay rank, final Short recurAfter,
final Date startDate, final MeetingType meetingType, final String meetingPlace, @SuppressWarnings("unused") final Locale locale) throws MeetingException {
this.validateFields(recurrenceType, startDate, meetingType, meetingPlace);
this.meetingDetails = new MeetingDetailsEntity(new RecurrenceTypeEntity(recurrenceType), dayNumber, weekDay,
rank, recurAfter, this);
if (meetingType != null) {
this.meetingType = new MeetingTypeEntity(meetingType);
}
this.meetingId = null;
this.meetingStartDate = DateUtils.getDateWithoutTimeStamp(startDate.getTime());
this.meetingPlace = meetingPlace;
}
public MeetingBO(final Short dayNumber, final Short recurAfter, final Date startDate, final MeetingType meetingType, final String meetingPlace,
final Short weekNumber) throws MeetingException {
this(RecurrenceType.MONTHLY, null, WeekDay.getWeekDay(dayNumber), RankOfDay.getRankOfDay(weekNumber), recurAfter,
startDate, meetingType, meetingPlace);
}
public MeetingBO(final int recurrenceId, final Short dayNumber, final Short recurAfter, final Date startDate, final MeetingType meetingType,
final String meetingPlace) throws MeetingException {
this(RecurrenceType.WEEKLY, null, WeekDay.getWeekDay(dayNumber), null, recurAfter, startDate, meetingType,
meetingPlace);
}
public MeetingDetailsEntity getMeetingDetails() {
return meetingDetails;
}
public Integer getMeetingId() {
return meetingId;
}
public String getMeetingPlace() {
return meetingPlace;
}
public void setMeetingPlace(final String meetingPlace) {
this.meetingPlace = meetingPlace;
}
public Date getMeetingStartDate() {
return meetingStartDate;
}
public void setMeetingStartDate(final Date meetingStartDate) {
this.meetingStartDate = DateUtils.getDateWithoutTimeStamp(meetingStartDate);
}
public void setStartDate(final Date startDate) {
this.meetingStartDate = startDate;
}
public final Date getStartDate() {
return meetingStartDate;
}
public MeetingTypeEntity getMeetingType() {
return meetingType;
}
public MeetingType getMeetingTypeEnum() {
return meetingType.asEnum();
}
public void setMeetingType(final MeetingTypeEntity meetingType) {
this.meetingType = meetingType;
}
public boolean isMonthlyOnDate() {
return getMeetingDetails().isMonthlyOnDate();
}
public boolean isWeekly() {
return getMeetingDetails().isWeekly();
}
public boolean isMonthly() {
return getMeetingDetails().isMonthly();
}
public boolean isDaily() {
return getMeetingDetails().isDaily();
}
public void update(final WeekDay weekDay, final String meetingPlace) throws MeetingException {
validateMeetingPlace(meetingPlace);
getMeetingDetails().getMeetingRecurrence().updateWeekDay(weekDay);
this.meetingPlace = meetingPlace;
}
public void update(final WeekDay weekDay, final RankOfDay rank, final String meetingPlace) throws MeetingException {
validateMeetingPlace(meetingPlace);
getMeetingDetails().getMeetingRecurrence().update(weekDay, rank);
this.meetingPlace = meetingPlace;
}
public void update(final Short dayNumber, final String meetingPlace) throws MeetingException {
validateMeetingPlace(meetingPlace);
getMeetingDetails().getMeetingRecurrence().updateDayNumber(dayNumber);
this.meetingPlace = meetingPlace;
}
public void update(final String meetingPlace) throws MeetingException {
validateMeetingPlace(meetingPlace);
this.meetingPlace = meetingPlace;
}
private void validateFields(final RecurrenceType recurrenceType, final Date startDate, final MeetingType meetingType,
final String meetingPlace) throws MeetingException {
if (recurrenceType == null) {
throw new MeetingException(MeetingConstants.INVALID_RECURRENCETYPE);
}
if (startDate == null) {
throw new MeetingException(MeetingConstants.INVALID_STARTDATE);
}
if (meetingType == null) {
throw new MeetingException(MeetingConstants.INVALID_MEETINGTYPE);
}
validateMeetingPlace(meetingPlace);
}
private void validateMeetingPlace(final String meetingPlace) throws MeetingException {
if (StringUtils.isBlank(meetingPlace)) {
throw new MeetingException(MeetingConstants.INVALID_MEETINGPLACE);
}
}
public boolean isValidMeetingDateUntilNextYear(final Date meetingDate) throws MeetingException {
return isValidMeetingDate(meetingDate, DateUtils.getLastDayOfNextYear());
}
public boolean isValidMeetingDate(final Date meetingDate, final Date endDate) throws MeetingException {
validateMeetingDate(meetingDate);
validateEndDate(endDate);
DateTime currentScheduleDateTime = findNearestMatchingDate(new DateTime(this.meetingStartDate));
Date currentScheduleDate = currentScheduleDateTime.toDate();
Calendar c = Calendar.getInstance();
c.setTime(currentScheduleDate);
currentScheduleDate = getNextWorkingDay(c).getTime();
Date meetingDateWOTimeStamp = DateUtils.getDateWithoutTimeStamp(meetingDate.getTime());
Date endDateWOTimeStamp = DateUtils.getDateWithoutTimeStamp(endDate.getTime());
if (meetingDateWOTimeStamp.compareTo(endDateWOTimeStamp) > 0) {
return false;
}
while (currentScheduleDate.compareTo(meetingDateWOTimeStamp) < 0
&& currentScheduleDate.compareTo(endDateWOTimeStamp) < 0) {
currentScheduleDate = findNextMatchingDate(new DateTime(currentScheduleDate)).toDate();
c.setTime(currentScheduleDate);
currentScheduleDate = getNextWorkingDay(c).getTime();
}
boolean isRepaymentIndepOfMeetingEnabled = new ConfigurationPersistence().isRepaymentIndepOfMeetingEnabled();
if (isRepaymentIndepOfMeetingEnabled) {
return currentScheduleDate.compareTo(endDateWOTimeStamp) <= 0;
}
// If repayment date is dependend on meeting date, then they need to
// match
return currentScheduleDate.compareTo(endDateWOTimeStamp) <= 0
&& currentScheduleDate.compareTo(meetingDateWOTimeStamp) == 0;
}
private Calendar getNextWorkingDay(final Calendar day) {
while (!new FiscalCalendarRules().isWorkingDay(day)) {
day.add(Calendar.DATE, 1);
}
return day;
}
public boolean isValidMeetingDate(final Date meetingDate, final int occurrences) throws MeetingException {
validateMeetingDate(meetingDate);
validateOccurences(occurrences);
DateTime currentScheduleDateTime = findNearestMatchingDate(new DateTime(this.meetingStartDate));
Date currentScheduleDate = currentScheduleDateTime.toDate();
Date meetingDateWOTimeStamp = DateUtils.getDateWithoutTimeStamp(meetingDate.getTime());
for (int currentNumber = 1; currentScheduleDate.compareTo(meetingDateWOTimeStamp) < 0
&& currentNumber < occurrences; currentNumber++) {
currentScheduleDate = findNextMatchingDate(new DateTime(currentScheduleDate)).toDate();
}
boolean isRepaymentIndepOfMeetingEnabled = new ConfigurationPersistence().isRepaymentIndepOfMeetingEnabled();
if (!isRepaymentIndepOfMeetingEnabled) {
// If repayment date is dependend on meeting date, then they need to
// match
return currentScheduleDate.compareTo(meetingDateWOTimeStamp) == 0;
}
return true;
}
public Date getNextScheduleDateAfterRecurrenceWithoutAdjustment(final Date afterDate) throws MeetingException {
validateMeetingDate(afterDate);
DateTime from = findNearestMatchingDate(new DateTime(this.meetingStartDate));
DateTime currentScheduleDate = findNextMatchingDate(from);
while (currentScheduleDate.toDate().compareTo(afterDate) <= 0) {
currentScheduleDate = findNextMatchingDate(currentScheduleDate);
}
return currentScheduleDate.toDate();
}
private DateTime findNearestMatchingDate(DateTime startingFrom) {
ScheduledEvent scheduledEvent = ScheduledEventFactory.createScheduledEventFrom(this);
return scheduledEvent.nearestMatchingDateBeginningAt(startingFrom);
}
public Date getPrevScheduleDateAfterRecurrence(final Date meetingDate) throws MeetingException {
validateMeetingDate(meetingDate);
DateTime prevScheduleDate = null;
/*
* Current schedule date as next meeting date after start date till this
* date is after given meeting date or increment current schedule date
* to next meeting date from current schedule date return the last but
* one current schedule date as prev schedule date
*/
DateTime currentScheduleDate = findNextMatchingDate(new DateTime(this.meetingStartDate));
while (currentScheduleDate.toDate().compareTo(meetingDate) < 0) {
prevScheduleDate = currentScheduleDate;
currentScheduleDate = findNextMatchingDate(currentScheduleDate);
}
if (prevScheduleDate != null) {
return prevScheduleDate.toDate();
}
return null;
}
private void validateMeetingDate(final Date meetingDate) throws MeetingException {
if (meetingDate == null) {
throw new MeetingException(MeetingConstants.INVALID_MEETINGDATE);
}
}
private void validateOccurences(final int occurrences) throws MeetingException {
if (occurrences <= 0) {
throw new MeetingException(MeetingConstants.INVALID_OCCURENCES);
}
}
private void validateEndDate(final Date endDate) throws MeetingException {
if (endDate == null || endDate.compareTo(getStartDate()) < 0) {
throw new MeetingException(MeetingConstants.INVALID_ENDDATE);
}
}
private DateTime findNextMatchingDate(DateTime startingFrom) {
ScheduledEvent scheduledEvent = ScheduledEventFactory.createScheduledEventFrom(this);
return scheduledEvent.nextEventDateAfter(startingFrom);
}
/*
* This seems like it is trying to answer the question of whether meetings
* for meetingToBeMatched and meetingToBeMatchedWith overlap. For example a
* weekly meeting occurring every 2 weeks potentially overlaps with a
* meeting occurring every 4 weeks.
*/
public static boolean isMeetingMatched(final MeetingBO meetingToBeMatched, final MeetingBO meetingToBeMatchedWith) {
return meetingToBeMatched != null
&& meetingToBeMatchedWith != null
&& meetingToBeMatched.getMeetingDetails().getRecurrenceType().getRecurrenceId().equals(
meetingToBeMatchedWith.getMeetingDetails().getRecurrenceType().getRecurrenceId())
&& isMultiple(meetingToBeMatchedWith.getMeetingDetails().getRecurAfter(), meetingToBeMatched
.getMeetingDetails().getRecurAfter());
}
private static boolean isMultiple(final Short valueToBeChecked, final Short valueToBeCheckedWith) {
return valueToBeChecked % valueToBeCheckedWith == 0;
}
public void setMeetingDetails(final MeetingDetailsEntity meetingDetails) {
this.meetingDetails = meetingDetails;
}
public RecurrenceType getRecurrenceType() {
return meetingDetails.getRecurrenceTypeEnum();
}
public Short getRecurAfter() {
return meetingDetails.getRecurAfter();
}
/*
* Get the start date of the "interval" surrounding a given date
* For example assume March 1 is a Monday and that weeks are defined to start on
* Monday. If this is a weekly meeting on a Wednesday then the "interval"
* for Wednesday March 10 is the week from Monday March 8 to Sunday March 14,
* and this method would return March 8.
*/
public LocalDate startDateForMeetingInterval(LocalDate date) {
LocalDate startOfMeetingInterval = date;
if (isWeekly()) {
int weekDay = WeekDay.getJodaDayOfWeekThatMatchesMifosWeekDay(getFiscalCalendarRules().getStartOfWeekWeekDay().getValue());
while (startOfMeetingInterval.getDayOfWeek() != weekDay) {
startOfMeetingInterval = startOfMeetingInterval.minusDays(1);
}
} else if (isMonthly()) {
int dayOfMonth = date.getDayOfMonth();
startOfMeetingInterval = startOfMeetingInterval.minusDays(dayOfMonth - 1);
} else {
// for days we return the same day
startOfMeetingInterval = date;
}
return startOfMeetingInterval;
}
public boolean queryDateIsInMeetingIntervalForFixedDate(LocalDate queryDate, LocalDate fixedDate) {
LocalDate startOfMeetingInterval = startDateForMeetingInterval(fixedDate);
LocalDate endOfMeetingInterval;
if (isWeekly()) {
endOfMeetingInterval = startOfMeetingInterval.plusWeeks(getRecurAfter());
} else if (isMonthly()) {
endOfMeetingInterval = startOfMeetingInterval.plusMonths(getRecurAfter());
} else {
// we don't handle meeting intervals in days
return false;
}
return (queryDate.isEqual(startOfMeetingInterval) ||
queryDate.isAfter(startOfMeetingInterval)) &&
queryDate.isBefore(endOfMeetingInterval);
}
public boolean hasSameRecurrenceAs(MeetingBO customerMeetingValue) {
return this.getRecurrenceType().equals(customerMeetingValue.getRecurrenceType());
}
public boolean recursOnMultipleOf(MeetingBO meeting) {
return meeting.getMeetingDetails().getRecurAfter().intValue() % this.meetingDetails.getRecurAfter().intValue() == 0;
}
public boolean isDayOfWeekDifferent(WeekDay dayOfWeek) {
return !dayOfWeek.equals(this.getMeetingDetails().getWeekDay());
}
public boolean isDayOfMonthDifferent(Short dayOfMonth) {
return !dayOfMonth.equals(this.getMeetingDetails().getDayNumber());
}
public boolean isWeekOfMonthDifferent(RankOfDay weekOfMonth, WeekDay dayOfWeekInWeekOfMonth) {
boolean isDifferent = false;
if (!weekOfMonth.equals(this.getMeetingDetails().getWeekRank())) {
isDifferent = true;
}
if (!dayOfWeekInWeekOfMonth.equals(this.getMeetingDetails().getWeekDay())) {
isDifferent = true;
}
return isDifferent;
}
public MeetingDto toDto() {
MeetingTypeDto meetingType = this.meetingType.toDto();
MeetingDetailsDto meetingDetailsDto = this.meetingDetails.toDto();
return new MeetingDto(new LocalDate(this.meetingStartDate), this.meetingPlace, meetingType, meetingDetailsDto);
}
}