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}