001    /**
002     * Copyright 2004-2012 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.hr.time.detail.validation;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.joda.time.*;
020    import org.kuali.hr.time.assignment.Assignment;
021    import org.kuali.hr.time.assignment.AssignmentDescriptionKey;
022    import org.kuali.hr.time.calendar.CalendarEntries;
023    import org.kuali.hr.time.clocklog.ClockLog;
024    import org.kuali.hr.time.detail.web.TimeDetailActionFormBase;
025    import org.kuali.hr.time.earncode.EarnCode;
026    import org.kuali.hr.time.service.base.TkServiceLocator;
027    import org.kuali.hr.time.timeblock.TimeBlock;
028    import org.kuali.hr.time.timesheet.TimesheetDocument;
029    import org.kuali.hr.time.util.TKUser;
030    import org.kuali.hr.time.util.TKUtils;
031    import org.kuali.hr.time.util.TkConstants;
032    
033    import java.math.BigDecimal;
034    import java.sql.Date;
035    import java.sql.Timestamp;
036    import java.util.ArrayList;
037    import java.util.List;
038    
039    public class TimeDetailValidationService {
040    
041        /**
042         * Convenience method for handling validation directly from the form object.
043         * @param tdaf The populated form.
044         *
045         * @return A list of error strings.
046         */
047        public static List<String> validateTimeEntryDetails(TimeDetailActionFormBase tdaf) {
048            return TimeDetailValidationService.validateTimeEntryDetails(
049                    tdaf.getHours(), tdaf.getAmount(), tdaf.getStartTime(), tdaf.getEndTime(),
050                    tdaf.getStartDate(), tdaf.getEndDate(), tdaf.getTimesheetDocument(),
051                    tdaf.getSelectedEarnCode(), tdaf.getSelectedAssignment(),
052                    tdaf.getAcrossDays().equalsIgnoreCase("y"), tdaf.getTkTimeBlockId(), tdaf.getOvertimePref(), tdaf.getSpanningWeeks().equalsIgnoreCase("y")
053            );
054        }
055    
056        public static List<String> validateTimeEntryDetails(BigDecimal hours, BigDecimal amount, String startTimeS, String endTimeS, String startDateS, String endDateS, TimesheetDocument timesheetDocument, String selectedEarnCode, String selectedAssignment, boolean acrossDays, String timeblockId, String overtimePref, boolean spanningWeeks) {
057            List<String> errors = new ArrayList<String>();
058    
059            if (timesheetDocument == null) {
060                errors.add("No timesheet document found.");
061            }
062            if (errors.size() > 0) return errors;
063    
064            CalendarEntries payCalEntry = timesheetDocument.getPayCalendarEntry();
065            java.sql.Date asOfDate = payCalEntry.getEndPeriodDate();
066    
067            errors.addAll(TimeDetailValidationService.validateDates(startDateS, endDateS));
068            errors.addAll(TimeDetailValidationService.validateTimes(startTimeS, endTimeS));
069            if (errors.size() > 0) return errors;
070            // These methods use the UserTimeZone.
071            Long startTime = TKUtils.convertDateStringToTimestamp(startDateS, startTimeS).getTime();
072            Long endTime = TKUtils.convertDateStringToTimestamp(endDateS, endTimeS).getTime();
073    
074            errors.addAll(validateInterval(payCalEntry, startTime, endTime));
075            if (errors.size() > 0) return errors;
076    
077            EarnCode earnCode = new EarnCode();
078            if (StringUtils.isNotBlank(selectedEarnCode)) {
079                earnCode = TkServiceLocator.getEarnCodeService().getEarnCode(selectedEarnCode, asOfDate);
080                if (earnCode != null && earnCode.getRecordMethod()!= null && earnCode.getRecordMethod().equalsIgnoreCase(TkConstants.EARN_CODE_TIME)) {
081                    if (startTimeS == null) errors.add("The start time is blank.");
082                    if (endTimeS == null) errors.add("The end time is blank.");
083                    if (startTime - endTime == 0) errors.add("Start time and end time cannot be equivalent");
084                }
085            }
086            if (errors.size() > 0) return errors;
087    
088            DateTime startTemp = new DateTime(startTime);
089            DateTime endTemp = new DateTime(endTime);
090    
091            if (errors.size() == 0 && !acrossDays && !StringUtils.equals(TkConstants.EARN_CODE_CPE, overtimePref)) {
092                Hours hrs = Hours.hoursBetween(startTemp, endTemp);
093                if (hrs.getHours() >= 24) errors.add("One timeblock cannot exceed 24 hours");
094            }
095            if (errors.size() > 0) return errors;
096    
097            //Check that assignment is valid for both days
098            AssignmentDescriptionKey assignKey = TkServiceLocator.getAssignmentService().getAssignmentDescriptionKey(selectedAssignment);
099            Assignment assign = TkServiceLocator.getAssignmentService().getAssignment(assignKey, new Date(startTime));
100            if (assign == null) errors.add("Assignment is not valid for start date " + TKUtils.formatDate(new Date(startTime)));
101            assign = TkServiceLocator.getAssignmentService().getAssignment(assignKey, new Date(endTime));
102            if (assign == null) errors.add("Assignment is not valid for end date " + TKUtils.formatDate(new Date(endTime)));
103            if (errors.size() > 0) return errors;
104    
105            //------------------------
106            // some of the simple validations are in the js side in order to reduce the server calls
107            // 1. check if the begin / end time is empty - tk.calenadr.js
108            // 2. check the time format - timeparse.js
109            // 3. only allows decimals to be entered in the hour field
110            //------------------------
111    
112            //------------------------
113            // check if the begin / end time are valid
114            //------------------------
115            if ((startTime.compareTo(endTime) > 0 || endTime.compareTo(startTime) < 0)) {
116                errors.add("The time or date is not valid.");
117            }
118            if (errors.size() > 0) return errors;
119    
120            //------------------------
121            // check if the overnight shift is across days
122            //------------------------
123            if (acrossDays && hours == null && amount == null) {
124                if (startTemp.getHourOfDay() >= endTemp.getHourOfDay()
125                        && !(endTemp.getDayOfYear() - startTemp.getDayOfYear() <= 1
126                        && endTemp.getHourOfDay() == 0)) {
127                    errors.add("The \"apply to each day\" box should not be checked.");
128                }
129            }
130            if (errors.size() > 0) return errors;
131    
132            //------------------------
133            // Amount cannot be zero
134            //------------------------
135            if (amount != null && earnCode != null && StringUtils.equals(earnCode.getEarnCodeType(), TkConstants.EARN_CODE_AMOUNT)) {
136                if (amount.equals(BigDecimal.ZERO)) {
137                    errors.add("Amount cannot be zero.");
138                }
139                if (amount.scale() > 2) {
140                    errors.add("Amount cannot have more than two digits after decimal point.");
141                }
142            }
143            if (errors.size() > 0) return errors;
144    
145            //------------------------
146            // check if the hours entered for hourly earn code is greater than 24 hours per day
147            // Hours cannot be zero
148            //------------------------
149            if (hours != null && earnCode != null && StringUtils.equals(earnCode.getEarnCodeType(), TkConstants.EARN_CODE_HOUR)) {
150                if (hours.equals(BigDecimal.ZERO)) {
151                    errors.add("Hours cannot be zero.");
152                }
153                if (hours.scale() > 2) {
154                    errors.add("Hours cannot have more than two digits after decimal point.");
155                }
156                int dayDiff = endTemp.getDayOfYear() - startTemp.getDayOfYear() + 1;
157                if (hours.compareTo(new BigDecimal(dayDiff * 24)) == 1) {
158                    errors.add("Cannot enter more than 24 hours per day.");
159                }
160            }
161            if (errors.size() > 0) return errors;
162    
163    
164    
165            //------------------------
166            // check if time blocks overlap with each other. Note that the tkTimeBlockId is used to
167            // determine is it's updating an existing time block or adding a new one
168            //------------------------
169    
170            boolean isRegularEarnCode = StringUtils.equals(assign.getJob().getPayTypeObj().getRegEarnCode(),selectedEarnCode);
171            errors.addAll(validateOverlap(startTime, endTime, acrossDays, startDateS, endTimeS,startTemp, endTemp, timesheetDocument, timeblockId, isRegularEarnCode));
172            if (errors.size() > 0) return errors;
173    
174            // Accrual Hour Limits Validation
175            //errors.addAll(TkServiceLocator.getTimeOffAccrualService().validateAccrualHoursLimitByEarnCode(timesheetDocument, selectedEarnCode));
176    
177            return errors;
178        }
179    
180        public static List<String> validateOverlap(Long startTime, Long endTime, boolean acrossDays, String startDateS, String endTimeS, DateTime startTemp, DateTime endTemp, TimesheetDocument timesheetDocument, String timeblockId, boolean isRegularEarnCode) {
181            List<String> errors = new ArrayList<String>();
182            Interval addedTimeblockInterval = new Interval(startTime, endTime);
183            List<Interval> dayInt = new ArrayList<Interval>();
184    
185            //if the user is clocked in, check if this time block overlaps with the clock action
186            ClockLog lastClockLog = TkServiceLocator.getClockLogService().getLastClockLog(TKUser.getCurrentTargetPerson().getPrincipalId());
187            if(lastClockLog != null &&
188                            (lastClockLog.getClockAction().equals(TkConstants.CLOCK_IN) 
189                                            || lastClockLog.getClockAction().equals(TkConstants.LUNCH_IN))) {
190                     Timestamp lastClockTimestamp = lastClockLog.getClockTimestamp();
191                 String lastClockZone = lastClockLog.getClockTimestampTimezone();
192                 if (StringUtils.isEmpty(lastClockZone)) {
193                     lastClockZone = TKUtils.getSystemTimeZone();
194                 }
195                 DateTimeZone zone = DateTimeZone.forID(lastClockZone);
196                 DateTime clockWithZone = new DateTime(lastClockTimestamp, zone);
197                 DateTime currentTime = new DateTime(System.currentTimeMillis(), zone);
198                 Interval currentClockInInterval = new Interval(clockWithZone.getMillis(), currentTime.getMillis());
199    
200                if (isRegularEarnCode && addedTimeblockInterval.overlaps(currentClockInInterval)) {
201                     errors.add("The time block you are trying to add overlaps with the current clock action.");
202                     return errors;
203                 }
204            }
205           
206            if (acrossDays) {
207                DateTime start = new DateTime(startTime);
208                DateTime end = new DateTime(TKUtils.convertDateStringToTimestamp(startDateS, endTimeS).getTime());
209                if (endTemp.getDayOfYear() - startTemp.getDayOfYear() < 1) {
210                    end = new DateTime(endTime);
211                }
212                DateTime groupEnd = new DateTime(endTime);
213                Long startLong = start.getMillis();
214                Long endLong = end.getMillis();
215                //create interval span if start is before the end and the end is after the start except
216                //for when the end is midnight ..that converts to midnight of next day
217                DateMidnight midNight = new DateMidnight(endLong);
218                while (start.isBefore(groupEnd.getMillis()) && ((endLong >= startLong) || end.isEqual(midNight))) {
219                    Interval tempInt = null;
220                    if (end.isEqual(midNight)) {
221                        tempInt = addedTimeblockInterval;
222                    } else {
223                        tempInt = new Interval(startLong, endLong);
224                    }
225                    dayInt.add(tempInt);
226                    start = start.plusDays(1);
227                    end = end.plusDays(1);
228                    startLong = start.getMillis();
229                    endLong = end.getMillis();
230                }
231            } else {
232                dayInt.add(addedTimeblockInterval);
233            }
234    
235            for (TimeBlock timeBlock : timesheetDocument.getTimeBlocks()) {
236                if (errors.size() == 0 && StringUtils.equals(timeBlock.getEarnCodeType(), TkConstants.EARN_CODE_TIME)) {
237                    Interval timeBlockInterval = new Interval(timeBlock.getBeginTimestamp().getTime(), timeBlock.getEndTimestamp().getTime());
238                    for (Interval intv : dayInt) {
239                        if (timeBlockInterval.overlaps(intv) && (timeblockId == null || timeblockId.compareTo(timeBlock.getTkTimeBlockId()) != 0)) {
240                            errors.add("The time block you are trying to add overlaps with an existing time block.");
241                        }
242                    }
243                }
244            }
245    
246            return errors;
247        }
248    
249        public static List<String> validateDates(String startDateS, String endDateS) {
250            List<String> errors = new ArrayList<String>();
251            if (errors.size() == 0 && StringUtils.isEmpty(startDateS)) errors.add("The start date is blank.");
252            if (errors.size() == 0 && StringUtils.isEmpty(endDateS)) errors.add("The end date is blank.");
253            return errors;
254        }
255    
256        public static List<String> validateTimes(String startTimeS, String endTimeS) {
257            List<String> errors = new ArrayList<String>();
258            if (errors.size() == 0 && startTimeS == null) errors.add("The start time is blank.");
259            if (errors.size() == 0 && endTimeS == null) errors.add("The end time is blank.");
260            return errors;
261        }
262    
263        public static List<String> validateInterval(CalendarEntries payCalEntry, Long startTime, Long endTime) {
264            List<String> errors = new ArrayList<String>();
265            LocalDateTime pcb_ldt = payCalEntry.getBeginLocalDateTime();
266            LocalDateTime pce_ldt = payCalEntry.getEndLocalDateTime();
267            DateTimeZone utz = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
268            DateTime p_cal_b_dt = pcb_ldt.toDateTime(utz);
269            DateTime p_cal_e_dt = pce_ldt.toDateTime(utz);
270    
271            Interval payInterval = new Interval(p_cal_b_dt, p_cal_e_dt);
272            if (errors.size() == 0 && !payInterval.contains(startTime)) {
273                errors.add("The start date/time is outside the pay period");
274            }
275            if (errors.size() == 0 && !payInterval.contains(endTime) && p_cal_e_dt.getMillis() != endTime) {
276                errors.add("The end date/time is outside the pay period");
277            }
278            return errors;
279        }
280    
281    }