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