001/* 002 * Copyright 2006 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 */ 016package org.kuali.ole.fp.document.service.impl; 017 018import java.math.BigDecimal; 019import java.sql.Date; 020import java.sql.Timestamp; 021import java.text.ParseException; 022import java.util.Calendar; 023import java.util.Iterator; 024import java.util.List; 025 026import org.kuali.ole.fp.businessobject.TravelMileageRate; 027import org.kuali.ole.fp.document.dataaccess.TravelMileageRateDao; 028import org.kuali.ole.fp.document.service.DisbursementVoucherTravelService; 029import org.kuali.ole.sys.service.NonTransactional; 030import org.kuali.ole.sys.util.KfsDateUtils; 031import org.kuali.rice.core.api.datetime.DateTimeService; 032import org.kuali.rice.core.api.util.type.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 040public 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.ole.fp.document.service.DisbursementVoucherTravelService#calculatePerDiemAmount(org.kuali.ole.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 = KfsDateUtils.getDifferenceInDays(startDateTime, endDateTime); 097 double diffHours = KfsDateUtils.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.ole.fp.document.service.DisbursementVoucherTravelService#calculateMileageAmount(org.kuali.ole.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}