View Javadoc
1   /*
2    * The Kuali Financial System, a comprehensive financial management system for higher education.
3    * 
4    * Copyright 2005-2014 The Kuali Foundation
5    * 
6    * This program is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Affero General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   * 
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Affero General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Affero General Public License
17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.kuali.kfs.fp.document.service.impl;
20  
21  import java.math.BigDecimal;
22  import java.sql.Date;
23  import java.sql.Timestamp;
24  import java.text.ParseException;
25  import java.util.Calendar;
26  import java.util.Collection;
27  
28  import org.kuali.kfs.fp.businessobject.TravelMileageRate;
29  import org.kuali.kfs.fp.document.dataaccess.TravelMileageRateDao;
30  import org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService;
31  import org.kuali.kfs.sys.service.NonTransactional;
32  import org.kuali.kfs.sys.util.KfsDateUtils;
33  import org.kuali.rice.core.api.datetime.DateTimeService;
34  import org.kuali.rice.core.api.util.type.KualiDecimal;
35  
36  /**
37   * This is the default implementation of the DisbursementVoucherTravelService interface.
38   * Performs calculations of travel per diem and mileage amounts.
39   */
40  
41  @NonTransactional
42  public class DisbursementVoucherTravelServiceImpl implements DisbursementVoucherTravelService {
43      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DisbursementVoucherTravelServiceImpl.class);
44  
45      protected TravelMileageRateDao travelMileageRateDao;
46      protected DateTimeService dateTimeService;
47  
48      /**
49       * This method calculates the per diem amount for a given period of time at the rate provided.  The per diem amount is
50       * calculated as described below.
51       *
52       * For same day trips:
53       * - 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
54       * greater than 12 hours.  An additional 1/4 of a day is added back to the amount if the trip lasted past 7:00pm.
55       * - If the same day trip is less than 12 hours, the per diem amount will be zero.
56       *
57       * For multiple day trips:
58       * - 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
59       * during the trip that is not the first day or last day of the trip.
60       * - For the first day of the trip,
61       *   if the travel starts before noon, you receive a full day per diem,
62       *   if the travel starts between noon and 5:59pm, you get a half day per diem,
63       *   if the travel starts after 6:00pm, you only receive a quarter day per diem
64       * - For the last day of the trip,
65       *   if the travel ends before 6:00am, you only receive a quarter day per diem,
66       *   if the travel ends between 6:00am and noon, you receive a half day per diem,
67       *   if the travel ends after noon, you receive a full day per diem
68       *
69       * @param stateDateTime The starting date and time of the period the per diem amount is calculated for.
70       * @param endDateTime The ending date and time of the period the per diema mount is calculated for.
71       * @param rate The per diem rate used to calculate the per diem amount.
72       * @return The per diem amount for the period specified, at the rate given.
73       *
74       * @see org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService#calculatePerDiemAmount(org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel)
75       */
76      @Override
77      public KualiDecimal calculatePerDiemAmount(Timestamp startDateTime, Timestamp endDateTime, KualiDecimal rate) {
78          KualiDecimal perDiemAmount = KualiDecimal.ZERO;
79          KualiDecimal perDiemRate = new KualiDecimal(rate.doubleValue());
80  
81          // make sure we have the fields needed
82          if (perDiemAmount == null || startDateTime == null || endDateTime == null) {
83              LOG.error("Per diem amount, Start date/time, and End date/time must all be given.");
84              throw new RuntimeException("Per diem amount, Start date/time, and End date/time must all be given.");
85          }
86  
87          // check end time is after start time
88          if (endDateTime.compareTo(startDateTime) <= 0) {
89              LOG.error("End date/time must be after start date/time.");
90              throw new RuntimeException("End date/time must be after start date/time.");
91          }
92  
93          Calendar startCalendar = Calendar.getInstance();
94          startCalendar.setTime(startDateTime);
95  
96          Calendar endCalendar = Calendar.getInstance();
97          endCalendar.setTime(endDateTime);
98  
99          double diffDays = KfsDateUtils.getDifferenceInDays(startDateTime, endDateTime);
100         double diffHours = KfsDateUtils.getDifferenceInHours(startDateTime, endDateTime);
101 
102         // same day travel
103         if (diffDays == 0) {
104             // no per diem for only 12 hours or less
105             if (diffHours > 12) {
106                 // half day of per diem
107                 perDiemAmount = perDiemRate.divide(new KualiDecimal(2));
108 
109                 // add in another 1/4 of a day if end time past 7:00
110                 if (timeInPerDiemPeriod(endCalendar, 19, 0, 23, 59)) {
111                     perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4)));
112                 }
113             }
114         }
115 
116         // multiple days of travel
117         else {
118             // must at least have 7 1/2 hours to get any per diem
119             if (diffHours >= 7.5) {
120                 // per diem for whole days
121                 perDiemAmount = perDiemRate.multiply(new KualiDecimal(diffDays - 1));
122 
123                 // per diem for first day
124                 if (timeInPerDiemPeriod(startCalendar, 0, 0, 11, 59)) { // Midnight to noon
125                     perDiemAmount = perDiemAmount.add(perDiemRate);
126                 }
127                 else if (timeInPerDiemPeriod(startCalendar, 12, 0, 17, 59)) { // Noon to 5:59pm
128                     perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(2)));
129                 }
130                 else if (timeInPerDiemPeriod(startCalendar, 18, 0, 23, 59)) { // 6:00pm to Midnight
131                     perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4)));
132                 }
133 
134                 // per diem for end day
135                 if (timeInPerDiemPeriod(endCalendar, 0, 1, 6, 0)) { // Midnight to 6:00am
136                     perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4)));
137                 }
138                 else if (timeInPerDiemPeriod(endCalendar, 6, 1, 12, 0)) { // 6:00am to noon
139                     perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(2)));
140                 }
141                 else if (timeInPerDiemPeriod(endCalendar, 12, 01, 23, 59)) { // Noon to midnight
142                     perDiemAmount = perDiemAmount.add(perDiemRate);
143                 }
144             }
145         }
146 
147         return perDiemAmount;
148     }
149 
150     /**
151      * Checks whether the date is in a per diem period given by the start hour and end hour and minutes.
152      *
153      * @param cal The date being checked to see if it occurred within the defined travel per diem period.
154      * @param periodStartHour The starting hour of the per diem period.
155      * @param periodStartMinute The starting minute of the per diem period.
156      * @param periodEndHour The ending hour of the per diem period.
157      * @param periodEndMinute The ending minute of the per diem period.
158      * @return True if the date passed in occurred within the period defined by the given parameters, false otherwise.
159      */
160     protected boolean timeInPerDiemPeriod(Calendar cal, int periodStartHour, int periodStartMinute, int periodEndHour, int periodEndMinute) {
161         int hour = cal.get(Calendar.HOUR_OF_DAY);
162         int minute = cal.get(Calendar.MINUTE);
163 
164         return (((hour > periodStartHour) || (hour == periodStartHour && minute >= periodStartMinute)) && ((hour < periodEndHour) || (hour == periodEndHour && minute <= periodEndMinute)));
165     }
166 
167     /**
168      * This method calculates the mileage amount based on the total mileage traveled and the using the reimbursement rate
169      * applicable to when the trip started.
170      *
171      * For this method, a collection of mileage rates is retrieved, where each mileage rate is defined by a mileage limit.
172      * This collection is iterated over to determine which mileage rate will be used for calculating the total mileage
173      * amount due.
174      *
175      * @param totalMileage The total mileage traveled that will be reimbursed for.
176      * @param travelStartDate The start date of the travel, which will be used to retrieve the mileage reimbursement rate.
177      * @return The total reimbursement due to the traveler for the mileage traveled.
178      *
179      * @see org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService#calculateMileageAmount(org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel)
180      */
181     @Override
182     public KualiDecimal calculateMileageAmount(Integer totalMileage, Timestamp travelStartDate) {
183         KualiDecimal mileageAmount = KualiDecimal.ZERO;
184 
185         if (totalMileage == null || travelStartDate == null) {
186             LOG.error("Total Mileage and Travel Start Date must be given.");
187             throw new RuntimeException("Total Mileage and Travel Start Date must be given.");
188         }
189 
190         // convert timestamp to sql date
191         Date effectiveDate = null;
192         try {
193             effectiveDate = dateTimeService.convertToSqlDate(travelStartDate);
194         } catch (ParseException e) {
195             LOG.error("Unable to parse travel start date into sql date " + travelStartDate, e);
196             throw new RuntimeException("Unable to parse travel start date into sql date ", e);
197         }
198 
199         // retrieve mileage rates
200         Collection<TravelMileageRate> mileageRates = travelMileageRateDao.retrieveMostEffectiveMileageRates(effectiveDate);
201 
202         if (mileageRates == null || mileageRates.isEmpty()) {
203             LOG.error("Unable to retreive mileage rates.");
204             throw new RuntimeException("Unable to retreive mileage rates.");
205         }
206 
207         int mileage = totalMileage.intValue();
208         int mileageRemaining = mileage;
209 
210         /**
211          * Iterate over mileage rates sorted in descending order by the mileage limit amount. For all miles over the mileage limit
212          * amount, the rate times those number of miles over is added to the mileage amount.
213          */
214         for ( TravelMileageRate rate : mileageRates ) {
215             int mileageLimitAmount = rate.getMileageLimitAmount().intValue();
216             if (mileageRemaining > mileageLimitAmount) {
217                 BigDecimal numMiles = new BigDecimal(mileageRemaining - mileageLimitAmount);
218                 BigDecimal rateForMiles = numMiles.multiply(rate.getMileageRate()).setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR);
219                 mileageAmount = mileageAmount.add(new KualiDecimal(rateForMiles));
220                 mileageRemaining = mileageLimitAmount;
221             }
222 
223         }
224 
225         return mileageAmount;
226     }
227 
228     /**
229      * Gets the travelMileageRateDao attribute.
230      * @return Returns the travelMileageRateDao.
231      */
232     public TravelMileageRateDao getTravelMileageRateDao() {
233         return travelMileageRateDao;
234     }
235 
236     /**
237      * Sets the travelMileageRateDao attribute.
238      * @param travelMileageRateDao The travelMileageRateDao to set.
239      */
240     public void setTravelMileageRateDao(TravelMileageRateDao travelMileageRateDao) {
241         this.travelMileageRateDao = travelMileageRateDao;
242     }
243 
244     /**
245      * Gets the dateTimeService attribute.
246      * @return Returns the dateTimeService.
247      */
248     public DateTimeService getDateTimeService() {
249         return dateTimeService;
250     }
251 
252     /**
253      * Sets the dateTimeService attribute.
254      * @param dateTimeService The dateTimeService to set.
255      */
256     public void setDateTimeService(DateTimeService dateTimeService) {
257         this.dateTimeService = dateTimeService;
258     }
259 }