/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.fp.document.service.impl; import java.math.BigDecimal; import java.sql.Date; import java.sql.Timestamp; import java.text.ParseException; import java.util.Calendar; import java.util.Collection; import org.kuali.kfs.fp.businessobject.TravelMileageRate; import org.kuali.kfs.fp.document.dataaccess.TravelMileageRateDao; import org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService; import org.kuali.kfs.sys.service.NonTransactional; import org.kuali.kfs.sys.util.KfsDateUtils; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.core.api.util.type.KualiDecimal; /** * This is the default implementation of the DisbursementVoucherTravelService interface. * Performs calculations of travel per diem and mileage amounts. */ @NonTransactional public class DisbursementVoucherTravelServiceImpl implements DisbursementVoucherTravelService { private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DisbursementVoucherTravelServiceImpl.class); protected TravelMileageRateDao travelMileageRateDao; protected DateTimeService dateTimeService; /** * This method calculates the per diem amount for a given period of time at the rate provided. The per diem amount is * calculated as described below. * * For same day trips: * - 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 * greater than 12 hours. An additional 1/4 of a day is added back to the amount if the trip lasted past 7:00pm. * - If the same day trip is less than 12 hours, the per diem amount will be zero. * * For multiple day trips: * - 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 * during the trip that is not the first day or last day of the trip. * - For the first day of the trip, * if the travel starts before noon, you receive a full day per diem, * if the travel starts between noon and 5:59pm, you get a half day per diem, * if the travel starts after 6:00pm, you only receive a quarter day per diem * - For the last day of the trip, * if the travel ends before 6:00am, you only receive a quarter day per diem, * if the travel ends between 6:00am and noon, you receive a half day per diem, * if the travel ends after noon, you receive a full day per diem * * @param stateDateTime The starting date and time of the period the per diem amount is calculated for. * @param endDateTime The ending date and time of the period the per diema mount is calculated for. * @param rate The per diem rate used to calculate the per diem amount. * @return The per diem amount for the period specified, at the rate given. * * @see org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService#calculatePerDiemAmount(org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel) */ @Override public KualiDecimal calculatePerDiemAmount(Timestamp startDateTime, Timestamp endDateTime, KualiDecimal rate) { KualiDecimal perDiemAmount = KualiDecimal.ZERO; KualiDecimal perDiemRate = new KualiDecimal(rate.doubleValue()); // make sure we have the fields needed if (perDiemAmount == null || startDateTime == null || endDateTime == null) { LOG.error("Per diem amount, Start date/time, and End date/time must all be given."); throw new RuntimeException("Per diem amount, Start date/time, and End date/time must all be given."); } // check end time is after start time if (endDateTime.compareTo(startDateTime) <= 0) { LOG.error("End date/time must be after start date/time."); throw new RuntimeException("End date/time must be after start date/time."); } Calendar startCalendar = Calendar.getInstance(); startCalendar.setTime(startDateTime); Calendar endCalendar = Calendar.getInstance(); endCalendar.setTime(endDateTime); double diffDays = KfsDateUtils.getDifferenceInDays(startDateTime, endDateTime); double diffHours = KfsDateUtils.getDifferenceInHours(startDateTime, endDateTime); // same day travel if (diffDays == 0) { // no per diem for only 12 hours or less if (diffHours > 12) { // half day of per diem perDiemAmount = perDiemRate.divide(new KualiDecimal(2)); // add in another 1/4 of a day if end time past 7:00 if (timeInPerDiemPeriod(endCalendar, 19, 0, 23, 59)) { perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4))); } } } // multiple days of travel else { // must at least have 7 1/2 hours to get any per diem if (diffHours >= 7.5) { // per diem for whole days perDiemAmount = perDiemRate.multiply(new KualiDecimal(diffDays - 1)); // per diem for first day if (timeInPerDiemPeriod(startCalendar, 0, 0, 11, 59)) { // Midnight to noon perDiemAmount = perDiemAmount.add(perDiemRate); } else if (timeInPerDiemPeriod(startCalendar, 12, 0, 17, 59)) { // Noon to 5:59pm perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(2))); } else if (timeInPerDiemPeriod(startCalendar, 18, 0, 23, 59)) { // 6:00pm to Midnight perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4))); } // per diem for end day if (timeInPerDiemPeriod(endCalendar, 0, 1, 6, 0)) { // Midnight to 6:00am perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(4))); } else if (timeInPerDiemPeriod(endCalendar, 6, 1, 12, 0)) { // 6:00am to noon perDiemAmount = perDiemAmount.add(perDiemRate.divide(new KualiDecimal(2))); } else if (timeInPerDiemPeriod(endCalendar, 12, 01, 23, 59)) { // Noon to midnight perDiemAmount = perDiemAmount.add(perDiemRate); } } } return perDiemAmount; } /** * Checks whether the date is in a per diem period given by the start hour and end hour and minutes. * * @param cal The date being checked to see if it occurred within the defined travel per diem period. * @param periodStartHour The starting hour of the per diem period. * @param periodStartMinute The starting minute of the per diem period. * @param periodEndHour The ending hour of the per diem period. * @param periodEndMinute The ending minute of the per diem period. * @return True if the date passed in occurred within the period defined by the given parameters, false otherwise. */ protected boolean timeInPerDiemPeriod(Calendar cal, int periodStartHour, int periodStartMinute, int periodEndHour, int periodEndMinute) { int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); return (((hour > periodStartHour) || (hour == periodStartHour && minute >= periodStartMinute)) && ((hour < periodEndHour) || (hour == periodEndHour && minute <= periodEndMinute))); } /** * This method calculates the mileage amount based on the total mileage traveled and the using the reimbursement rate * applicable to when the trip started. * * For this method, a collection of mileage rates is retrieved, where each mileage rate is defined by a mileage limit. * This collection is iterated over to determine which mileage rate will be used for calculating the total mileage * amount due. * * @param totalMileage The total mileage traveled that will be reimbursed for. * @param travelStartDate The start date of the travel, which will be used to retrieve the mileage reimbursement rate. * @return The total reimbursement due to the traveler for the mileage traveled. * * @see org.kuali.kfs.fp.document.service.DisbursementVoucherTravelService#calculateMileageAmount(org.kuali.kfs.fp.businessobject.DisbursementVoucherNonEmployeeTravel) */ @Override public KualiDecimal calculateMileageAmount(Integer totalMileage, Timestamp travelStartDate) { KualiDecimal mileageAmount = KualiDecimal.ZERO; if (totalMileage == null || travelStartDate == null) { LOG.error("Total Mileage and Travel Start Date must be given."); throw new RuntimeException("Total Mileage and Travel Start Date must be given."); } // convert timestamp to sql date Date effectiveDate = null; try { effectiveDate = dateTimeService.convertToSqlDate(travelStartDate); } catch (ParseException e) { LOG.error("Unable to parse travel start date into sql date " + travelStartDate, e); throw new RuntimeException("Unable to parse travel start date into sql date ", e); } // retrieve mileage rates Collection<TravelMileageRate> mileageRates = travelMileageRateDao.retrieveMostEffectiveMileageRates(effectiveDate); if (mileageRates == null || mileageRates.isEmpty()) { LOG.error("Unable to retreive mileage rates."); throw new RuntimeException("Unable to retreive mileage rates."); } int mileage = totalMileage.intValue(); int mileageRemaining = mileage; /** * Iterate over mileage rates sorted in descending order by the mileage limit amount. For all miles over the mileage limit * amount, the rate times those number of miles over is added to the mileage amount. */ for ( TravelMileageRate rate : mileageRates ) { int mileageLimitAmount = rate.getMileageLimitAmount().intValue(); if (mileageRemaining > mileageLimitAmount) { BigDecimal numMiles = new BigDecimal(mileageRemaining - mileageLimitAmount); BigDecimal rateForMiles = numMiles.multiply(rate.getMileageRate()).setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR); mileageAmount = mileageAmount.add(new KualiDecimal(rateForMiles)); mileageRemaining = mileageLimitAmount; } } return mileageAmount; } /** * Gets the travelMileageRateDao attribute. * @return Returns the travelMileageRateDao. */ public TravelMileageRateDao getTravelMileageRateDao() { return travelMileageRateDao; } /** * Sets the travelMileageRateDao attribute. * @param travelMileageRateDao The travelMileageRateDao to set. */ public void setTravelMileageRateDao(TravelMileageRateDao travelMileageRateDao) { this.travelMileageRateDao = travelMileageRateDao; } /** * Gets the dateTimeService attribute. * @return Returns the dateTimeService. */ public DateTimeService getDateTimeService() { return dateTimeService; } /** * Sets the dateTimeService attribute. * @param dateTimeService The dateTimeService to set. */ public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } }