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 }