001 /*
002 * Copyright 2011 The Kuali Foundation.
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.kfs.fp.document.service.impl;
017
018 import java.math.BigDecimal;
019 import java.sql.Date;
020 import java.sql.Timestamp;
021 import java.text.ParseException;
022 import java.util.Calendar;
023 import java.util.Iterator;
024 import java.util.List;
025
026 import org.kuali.kfs.fp.businessobject.TravelMileageRate;
027 import org.kuali.kfs.fp.document.dataaccess.TravelMileageRateDao;
028 import org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService;
029 import org.kuali.kfs.sys.service.NonTransactional;
030 import org.kuali.rice.kns.service.DateTimeService;
031 import org.kuali.rice.kns.util.DateUtils;
032 import org.kuali.rice.kns.util.KualiDecimal;
033
034 /**
035 * This is the default implementation of the DisbursementVoucherTravelService interface.
036 * Performs calculations of travel per diem and mileage amounts.
037 */
038
039 @NonTransactional
040 public class DisbursementVoucherTravelServiceImpl implements DisbursementVoucherTravelService {
041 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DisbursementVoucherTravelServiceImpl.class);
042
043 private TravelMileageRateDao travelMileageRateDao;
044 private DateTimeService dateTimeService;
045
046 /**
047 * This method calculates the per diem amount for a given period of time at the rate provided. The per diem amount is
048 * calculated as described below.
049 *
050 * For same day trips:
051 * - Per diem is equal to 1/2 of the per diem rate provided if the difference in time between the start and end time is
052 * greater than 12 hours. An additional 1/4 of a day is added back to the amount if the trip lasted past 7:00pm.
053 * - If the same day trip is less than 12 hours, the per diem amount will be zero.
054 *
055 * For multiple day trips:
056 * - Per diem amount is equal to the full rate times the number of full days of travel. A full day is equal to any day
057 * during the trip that is not the first day or last day of the trip.
058 * - For the first day of the trip,
059 * if the travel starts before noon, you receive a full day per diem,
060 * if the travel starts between noon and 5:59pm, you get a half day per diem,
061 * if the travel starts after 6:00pm, you only receive a quarter day per diem
062 * - For the last day of the trip,
063 * if the travel ends before 6:00am, you only receive a quarter day per diem,
064 * if the travel ends between 6:00am and noon, you receive a half day per diem,
065 * if the travel ends after noon, you receive a full day per diem
066 *
067 * @param stateDateTime The starting date and time of the period the per diem amount is calculated for.
068 * @param endDateTime The ending date and time of the period the per diema mount is calculated for.
069 * @param rate The per diem rate used to calculate the per diem amount.
070 * @return The per diem amount for the period specified, at the rate given.
071 *
072 * @see org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService#calculatePerDiemAmount(org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel)
073 */
074 public KualiDecimal calculatePerDiemAmount(Timestamp startDateTime, Timestamp endDateTime, KualiDecimal rate) {
075 KualiDecimal perDiemAmount = KualiDecimal.ZERO;
076 KualiDecimal perDiemRate = new KualiDecimal(rate.doubleValue());
077
078 // make sure we have the fields needed
079 if (perDiemAmount == null || startDateTime == null || endDateTime == null) {
080 LOG.error("Per diem amount, Start date/time, and End date/time must all be given.");
081 throw new RuntimeException("Per diem amount, Start date/time, and End date/time must all be given.");
082 }
083
084 // check end time is after start time
085 if (endDateTime.compareTo(startDateTime) <= 0) {
086 LOG.error("End date/time must be after start date/time.");
087 throw new RuntimeException("End date/time must be after start date/time.");
088 }
089
090 Calendar startCalendar = Calendar.getInstance();
091 startCalendar.setTime(startDateTime);
092
093 Calendar endCalendar = Calendar.getInstance();
094 endCalendar.setTime(endDateTime);
095
096 double diffDays = DateUtils.getDifferenceInDays(startDateTime, endDateTime);
097 double diffHours = DateUtils.getDifferenceInHours(startDateTime, endDateTime);
098
099 // same day travel
100 if (diffDays == 0) {
101 // no per diem for only 12 hours or less
102 if (diffHours > 12) {
103 // half day of per diem
104 perDiemAmount = perDiemRate.divide(new KualiDecimal(2));
105
106 // add in another 1/4 of a day if end time past 7:00
107 if (timeInPerDiemPeriod(endCalendar, 19, 0, 23, 59)) {
108 perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4)));
109 }
110 }
111 }
112
113 // multiple days of travel
114 else {
115 // must at least have 7 1/2 hours to get any per diem
116 if (diffHours >= 7.5) {
117 // per diem for whole days
118 perDiemAmount = perDiemRate.multiply(new KualiDecimal(diffDays - 1));
119
120 // per diem for first day
121 if (timeInPerDiemPeriod(startCalendar, 0, 0, 11, 59)) { // Midnight to noon
122 perDiemAmount = perDiemAmount.add(perDiemRate);
123 }
124 else if (timeInPerDiemPeriod(startCalendar, 12, 0, 17, 59)) { // Noon to 5:59pm
125 perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(2)));
126 }
127 else if (timeInPerDiemPeriod(startCalendar, 18, 0, 23, 59)) { // 6:00pm to Midnight
128 perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4)));
129 }
130
131 // per diem for end day
132 if (timeInPerDiemPeriod(endCalendar, 0, 1, 6, 0)) { // Midnight to 6:00am
133 perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4)));
134 }
135 else if (timeInPerDiemPeriod(endCalendar, 6, 1, 12, 0)) { // 6:00am to noon
136 perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(2)));
137 }
138 else if (timeInPerDiemPeriod(endCalendar, 12, 01, 23, 59)) { // Noon to midnight
139 perDiemAmount = perDiemAmount.add(perDiemRate);
140 }
141 }
142 }
143
144 return perDiemAmount;
145 }
146
147 /**
148 * Checks whether the date is in a per diem period given by the start hour and end hour and minutes.
149 *
150 * @param cal The date being checked to see if it occurred within the defined travel per diem period.
151 * @param periodStartHour The starting hour of the per diem period.
152 * @param periodStartMinute The starting minute of the per diem period.
153 * @param periodEndHour The ending hour of the per diem period.
154 * @param periodEndMinute The ending minute of the per diem period.
155 * @return True if the date passed in occurred within the period defined by the given parameters, false otherwise.
156 */
157 protected boolean timeInPerDiemPeriod(Calendar cal, int periodStartHour, int periodStartMinute, int periodEndHour, int periodEndMinute) {
158 int hour = cal.get(Calendar.HOUR_OF_DAY);
159 int minute = cal.get(Calendar.MINUTE);
160
161 return (((hour > periodStartHour) || (hour == periodStartHour && minute >= periodStartMinute)) && ((hour < periodEndHour) || (hour == periodEndHour && minute <= periodEndMinute)));
162 }
163
164 /**
165 * This method calculates the mileage amount based on the total mileage traveled and the using the reimbursement rate
166 * applicable to when the trip started.
167 *
168 * For this method, a collection of mileage rates is retrieved, where each mileage rate is defined by a mileage limit.
169 * This collection is iterated over to determine which mileage rate will be used for calculating the total mileage
170 * amount due.
171 *
172 * @param totalMileage The total mileage traveled that will be reimbursed for.
173 * @param travelStartDate The start date of the travel, which will be used to retrieve the mileage reimbursement rate.
174 * @return The total reimbursement due to the traveler for the mileage traveled.
175 *
176 * @see org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService#calculateMileageAmount(org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel)
177 */
178 public KualiDecimal calculateMileageAmount(Integer totalMileage, Timestamp travelStartDate) {
179 KualiDecimal mileageAmount = KualiDecimal.ZERO;
180
181 if (totalMileage == null || travelStartDate == null) {
182 LOG.error("Total Mileage and Travel Start Date must be given.");
183 throw new RuntimeException("Total Mileage and Travel Start Date must be given.");
184 }
185
186 // convert timestamp to sql date
187 Date effectiveDate = null;
188 try {
189 effectiveDate = dateTimeService.convertToSqlDate(travelStartDate);
190 }
191 catch (ParseException e) {
192 LOG.error("Unable to parse travel start date into sql date " + e.getMessage());
193 throw new RuntimeException("Unable to parse travel start date into sql date " + e.getMessage());
194 }
195
196 // retrieve mileage rates
197 List mileageRates = (List) travelMileageRateDao.retrieveMostEffectiveMileageRates(effectiveDate);
198
199 if (mileageRates == null || mileageRates.isEmpty()) {
200 LOG.error("Unable to retreive mileage rates.");
201 throw new RuntimeException("Unable to retreive mileage rates.");
202 }
203
204 int mileage = totalMileage.intValue();
205 int mileageRemaining = mileage;
206
207 /**
208 * Iterate over mileage rates sorted in descending order by the mileage limit amount. For all miles over the mileage limit
209 * amount, the rate times those number of miles over is added to the mileage amount.
210 */
211 for (Iterator iter = mileageRates.iterator(); iter.hasNext();) {
212 TravelMileageRate rate = (TravelMileageRate) iter.next();
213 int mileageLimitAmount = rate.getMileageLimitAmount().intValue();
214 if (mileageRemaining > mileageLimitAmount) {
215 BigDecimal numMiles = new BigDecimal(mileageRemaining - mileageLimitAmount);
216 BigDecimal rateForMiles = numMiles.multiply(rate.getMileageRate()).setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR);
217 mileageAmount = mileageAmount.add(new KualiDecimal(rateForMiles));
218 mileageRemaining = mileageLimitAmount;
219 }
220
221 }
222
223 return mileageAmount;
224 }
225
226 /**
227 * Gets the travelMileageRateDao attribute.
228 * @return Returns the travelMileageRateDao.
229 */
230 public TravelMileageRateDao getTravelMileageRateDao() {
231 return travelMileageRateDao;
232 }
233
234 /**
235 * Sets the travelMileageRateDao attribute.
236 * @param travelMileageRateDao The travelMileageRateDao to set.
237 */
238 public void setTravelMileageRateDao(TravelMileageRateDao travelMileageRateDao) {
239 this.travelMileageRateDao = travelMileageRateDao;
240 }
241
242 /**
243 * Gets the dateTimeService attribute.
244 * @return Returns the dateTimeService.
245 */
246 public DateTimeService getDateTimeService() {
247 return dateTimeService;
248 }
249
250 /**
251 * Sets the dateTimeService attribute.
252 * @param dateTimeService The dateTimeService to set.
253 */
254 public void setDateTimeService(DateTimeService dateTimeService) {
255 this.dateTimeService = dateTimeService;
256 }
257 }