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    }