View Javadoc

1   /**
2    * Copyright 2004-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.kpme.tklm.time.rules.shiftdifferential.service;
17  
18  import java.math.BigDecimal;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.Comparator;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.apache.commons.collections.CollectionUtils;
29  import org.apache.log4j.Logger;
30  import org.joda.time.DateTime;
31  import org.joda.time.DateTimeConstants;
32  import org.joda.time.DateTimeZone;
33  import org.joda.time.Duration;
34  import org.joda.time.Interval;
35  import org.joda.time.LocalDate;
36  import org.joda.time.LocalTime;
37  import org.kuali.kpme.core.KPMENamespace;
38  import org.kuali.kpme.core.calendar.entry.CalendarEntry;
39  import org.kuali.kpme.core.job.Job;
40  import org.kuali.kpme.core.permission.KPMEPermissionTemplate;
41  import org.kuali.kpme.core.principal.PrincipalHRAttributes;
42  import org.kuali.kpme.core.role.KPMERoleMemberAttribute;
43  import org.kuali.kpme.core.service.HrServiceLocator;
44  import org.kuali.kpme.core.util.HrConstants;
45  import org.kuali.kpme.core.util.TKUtils;
46  import org.kuali.kpme.tklm.time.rules.shiftdifferential.ShiftDifferentialRule;
47  import org.kuali.kpme.tklm.time.rules.shiftdifferential.dao.ShiftDifferentialRuleDao;
48  import org.kuali.kpme.tklm.time.service.TkServiceLocator;
49  import org.kuali.kpme.tklm.time.timeblock.TimeBlock;
50  import org.kuali.kpme.tklm.time.timehourdetail.TimeHourDetail;
51  import org.kuali.kpme.tklm.time.timesheet.TimesheetDocument;
52  import org.kuali.kpme.tklm.time.util.TkTimeBlockAggregate;
53  import org.kuali.kpme.tklm.time.workflow.TimesheetDocumentHeader;
54  import org.kuali.rice.kim.api.KimConstants;
55  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
56  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
57  
58  
59  public class ShiftDifferentialRuleServiceImpl implements ShiftDifferentialRuleService {
60  
61  	@SuppressWarnings("unused")
62  	private static final Logger LOG = Logger.getLogger(ShiftDifferentialRuleServiceImpl.class);
63  	/**
64  	 * The maximum allowable time between timeblocks before we consider them to
65  	 * be day-boundary single time blocks.
66  	 */
67  	private ShiftDifferentialRuleDao shiftDifferentialRuleDao = null;
68  
69  	private Map<Long,Set<ShiftDifferentialRule>> getJobNumberToShiftRuleMap(TimesheetDocument timesheetDocument) {
70  		Map<Long,Set<ShiftDifferentialRule>> jobNumberToShifts = new HashMap<Long,Set<ShiftDifferentialRule>>();
71  		PrincipalHRAttributes principalCal = HrServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(timesheetDocument.getPrincipalId(),timesheetDocument.getCalendarEntry().getEndPeriodFullDateTime().toLocalDate());
72  
73  		for (Job job : timesheetDocument.getJobs()) {
74  			List<ShiftDifferentialRule> shiftDifferentialRules = getShiftDifferentalRules(job.getLocation(),job.getHrSalGroup(),job.getPayGrade(),principalCal.getPayCalendar(), 
75  					timesheetDocument.getCalendarEntry().getEndPeriodFullDateTime().toLocalDate());
76  			if (shiftDifferentialRules.size() > 0)
77  				jobNumberToShifts.put(job.getJobNumber(), new HashSet<ShiftDifferentialRule>(shiftDifferentialRules));
78  		}
79  
80  		return jobNumberToShifts;
81  	}
82  
83  	private Map<Long,List<TimeBlock>> getPreviousPayPeriodLastDayJobToTimeBlockMap(TimesheetDocument timesheetDocument, Map<Long,Set<ShiftDifferentialRule>> jobNumberToShifts) {
84  		Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = null;
85  
86  		// Get the last day of the last week of the previous pay period.
87  		// This is the only day that can have impact on the current day.
88  		List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDateTime());
89  		if (prevBlocks.size() > 0) {
90  			TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDateTime().toDateTime());
91  			if (prevTdh != null) {
92  				CalendarEntry prevPayCalendarEntry = HrServiceLocator.getCalendarEntryService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getEndDateTime(), HrConstants.PAY_CALENDAR_TYPE);
93  				TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true);
94  				List<List<TimeBlock>> dayBlocks = prevTimeAggregate.getDayTimeBlockList();
95  				List<TimeBlock> previousPeriodLastDayBlocks = dayBlocks.get(dayBlocks.size() - 1);
96  				// Set back to null if there is nothing in the list.
97  				if (previousPeriodLastDayBlocks.size() > 0) {
98  					jobNumberToTimeBlocksPreviousDay = new HashMap<Long, List<TimeBlock>>();
99  
100 					for (TimeBlock block : previousPeriodLastDayBlocks) {
101 						// Job Number to TimeBlock for Last Day of Previous Time
102 						// Period
103 						Long jobNumber = block.getJobNumber();
104 						if (jobNumberToShifts.containsKey(jobNumber)) {
105 							// we have a useful timeblock.
106 							List<TimeBlock> jblist = jobNumberToTimeBlocksPreviousDay.get(jobNumber);
107 							if (jblist == null) {
108 								jblist = new ArrayList<TimeBlock>();
109 								jobNumberToTimeBlocksPreviousDay.put(jobNumber, jblist);
110 							}
111 							jblist.add(block);
112 						}
113 					}
114 				}
115 			}
116 		}
117 
118 		return jobNumberToTimeBlocksPreviousDay;
119 	}
120 
121 	private boolean timeBlockHasEarnCode(Set<String> earnCodes, TimeBlock block) {
122 		boolean present = false;
123 
124 		if (block != null && earnCodes != null)
125 			present = earnCodes.contains(block.getEarnCode());
126 
127 		return present;
128 	}
129 
130     /**
131      * Returns a BigDecimal representing the sum of all of the negative time
132      * hour detail types. In this case, only LUN is considered. This can be
133      * modified to add other considerations.
134      *
135      * @param block The Timeblock to inspect.
136      *
137      * @return A big decimal.
138      */
139     private BigDecimal negativeTimeHourDetailSum(TimeBlock block) {
140         BigDecimal sum = BigDecimal.ZERO;
141 
142         if (block != null) {
143             List<TimeHourDetail> details = block.getTimeHourDetails();
144             for (TimeHourDetail detail : details) {
145                 if (detail.getEarnCode().equals(HrConstants.LUNCH_EARN_CODE)) {
146                     sum = sum.add(detail.getHours());
147                 }
148             }
149         }
150 
151         return sum;
152     }
153 
154 	@Override
155 	public void processShiftDifferentialRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
156         DateTimeZone zone = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
157 		List<List<TimeBlock>> blockDays = aggregate.getDayTimeBlockList();
158 		DateTime periodStartDateTime = timesheetDocument.getCalendarEntry().getBeginPeriodLocalDateTime().toDateTime(zone);
159 		Map<Long,Set<ShiftDifferentialRule>> jobNumberToShifts = getJobNumberToShiftRuleMap(timesheetDocument);
160 
161 
162         // If there are no shift differential rules, we have an early exit.
163 		if (jobNumberToShifts.isEmpty()) {
164 			return;
165 		}
166 
167 		// Get the last day of the previous pay period. We need this to determine
168 		// if there are hours from the previous pay period that will effect the
169 		// shift rule on the first day of the currently-being-processed pay period.
170 		//
171         // Will be set to null if not applicable.
172         boolean previousPayPeriodPrevDay = true;
173 		Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay =
174                 getPreviousPayPeriodLastDayJobToTimeBlockMap(timesheetDocument, jobNumberToShifts);
175 
176 		// We are going to look at the time blocks grouped by Days.
177         //
178         // This is a very large outer loop.
179 		for (int pos = 0; pos < blockDays.size(); pos++) {
180 			List<TimeBlock> blocks = blockDays.get(pos); // Timeblocks for this day.
181 			if (blocks.isEmpty()) {
182 				continue; // No Time blocks, no worries.
183             }
184 			DateTime currentDay = periodStartDateTime.plusDays(pos);
185 			Interval virtualDay = new Interval(currentDay, currentDay.plusDays(1));
186 
187 			// Builds our JobNumber to TimeBlock for Current Day List.
188             //
189             // Shift Differential Rules are also grouped by Job number, this
190             // provides a quick way to do the lookup / reference.
191             // We don't need every time block, only the ones that will be
192             // applicable to the shift rules.
193 			Map<Long, List<TimeBlock>> jobNumberToTimeBlocks = new HashMap<Long,List<TimeBlock>>();
194 			for (TimeBlock block : blocks) {
195 				Long jobNumber = block.getJobNumber();
196 				if (jobNumberToShifts.containsKey(jobNumber)) {
197 					List<TimeBlock> jblist = jobNumberToTimeBlocks.get(jobNumber);
198 					if (jblist == null) {
199 						jblist = new ArrayList<TimeBlock>();
200 						jobNumberToTimeBlocks.put(jobNumber, jblist);
201 					}
202 					jblist.add(block);
203 				}
204 			}
205 
206 
207             // Large Outer Loop to look at applying the Shift Rules based on
208             // the current JobNumber.
209             //
210 			// This loop will handle previous day boundary time as well as the
211             // current day.
212             //
213             // There is room for refactoring here!
214 			for (Map.Entry<Long, Set<ShiftDifferentialRule>> entry : jobNumberToShifts.entrySet()) {
215 				Set<ShiftDifferentialRule> shiftDifferentialRules = entry.getValue();
216 				// Obtain and sort our previous and current time blocks.
217 				List<TimeBlock> ruleTimeBlocksPrev = null;
218 				List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(entry.getKey());
219 				if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) {
220 					if (jobNumberToTimeBlocksPreviousDay != null)
221 						ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(entry.getKey());
222 					if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0)
223 						this.sortTimeBlocksInverse(ruleTimeBlocksPrev);
224 					this.sortTimeBlocksNatural(ruleTimeBlocksCurr);
225 				} else {
226 					// Skip to next job, there is nothing for this job
227 					// on this day, and because of this we don't care
228 					// about the previous day either.
229 					continue;
230 				}
231 
232 				for (ShiftDifferentialRule rule : shiftDifferentialRules) {
233 					Set<String> fromEarnGroup = HrServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(rule.getFromEarnGroup(), timesheetDocument.getCalendarEntry().getBeginPeriodFullDateTime().toLocalDate());
234 
235                    /* LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
236                     LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);*/
237                     LocalTime ruleStart = new LocalTime(rule.getBeginTime());
238                     LocalTime ruleEnd = new LocalTime(rule.getEndTime());
239 
240 
241 					DateTime shiftEnd = ruleEnd.toDateTime(currentDay);
242 					DateTime shiftStart = ruleStart.toDateTime(currentDay);
243 
244 					if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) {
245 						shiftEnd = shiftEnd.plusDays(1);
246                     }
247 					Interval shiftInterval = new Interval(shiftStart, shiftEnd);
248 
249 					// Set up buckets to handle previous days time accumulations
250 					BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO;
251 
252 					// Check current day first block to see if start time gap from virtual day start is greater than max gap
253 					// if so, we can skip the previous day checks.
254 					TimeBlock firstBlockOfCurrentDay = null;
255 					for (TimeBlock b : ruleTimeBlocksCurr) {
256 						if (timeBlockHasEarnCode(fromEarnGroup, b)) {
257 							firstBlockOfCurrentDay = b;
258 							break;
259 						}
260 					}
261 
262 					// Previous Day :: We have prior block container of nonzero size, and the previous day is active.
263 					Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1));
264 
265                     // Blank initialization pointer for picking which interval to pass to applyPremium()
266                     Interval evalInterval = null;
267 					if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) {
268 						// Simple heuristic to see if we even need to worry about
269 						// the Shift rule for this set of data.
270 						if (shiftEnd.isAfter(virtualDay.getEnd())) {
271 							// Compare first block of previous day with first block of current day for max gaptitude.
272 							TimeBlock firstBlockOfPreviousDay = null;
273 							for (TimeBlock b : ruleTimeBlocksPrev) {
274 								if (timeBlockHasEarnCode(fromEarnGroup, b)) {
275 									firstBlockOfPreviousDay = b;
276 									break;
277 								}
278 							}
279 							// Only if we actually have at least one block.
280                             // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day.
281                             // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which
282                             // blocks are valid.
283 							if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) {
284 								Interval previousBlockInterval = new Interval(firstBlockOfPreviousDay.getEndDateTime().withZone(zone), firstBlockOfCurrentDay.getBeginDateTime().withZone(zone));
285 								Duration blockGapDuration = previousBlockInterval.toDuration();
286 								BigDecimal bgdMinutes = TKUtils.convertMillisToMinutes(blockGapDuration.getMillis());
287 								// if maxGap is 0, ignore gaps and assign shift to time blocks within the hours
288 								if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdMinutes.compareTo(rule.getMaxGap()) <= 0) {
289 									// If we are here, we know we have at least one valid time block to pull some hours forward from.
290 
291 
292 									// These are inversely sorted.
293 									for (int i=0; i<ruleTimeBlocksPrev.size(); i++) {
294 										TimeBlock b = ruleTimeBlocksPrev.get(i);
295 										if (timeBlockHasEarnCode(fromEarnGroup, b)) {
296 											Interval blockInterval = new Interval(b.getBeginDateTime().withZone(zone), b.getEndDateTime().withZone(zone));
297 
298 											// Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks.
299 											if (previousBlockInterval != null) {
300 												blockGapDuration = new Duration(b.getEndDateTime().withZone(zone), previousBlockInterval.getStart());
301                                                 bgdMinutes = TKUtils.convertMillisToMinutes(blockGapDuration.getMillis());
302 											}
303 
304 											// Check Gap, if good, sum hours, if maxGap is 0, ignore gaps
305 											if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdMinutes.compareTo(rule.getMaxGap()) <= 0) {
306 												// Calculate Overlap and add it to hours before virtual day bucket.												
307 												if (blockInterval.overlaps(previousDayShiftInterval)) {
308 													boolean ruleAppliedAlready = false;
309 													if(CollectionUtils.isNotEmpty(b.getTimeHourDetails())) {														
310 														for(TimeHourDetail tbd : b.getTimeHourDetails()) {
311 															if(tbd.getEarnCode().equals(rule.getEarnCode())) {
312 																ruleAppliedAlready = true;
313 															}
314 														}
315 													}
316 													// if this time block already had this rule applied to it, no need to calculate the same hours again
317 													if(!ruleAppliedAlready) {
318 														BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis());
319 														hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs);
320 													}
321 												}
322 
323 											} else {
324 												// Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded.
325 												break;
326 											}
327 
328 											previousBlockInterval = blockInterval;
329 
330 										}
331 									}
332 								} else {
333 									// DO NOTHING!
334 								}
335 							}
336 						}
337 					}
338 
339 					BigDecimal hoursToApply = BigDecimal.ZERO;
340 					BigDecimal hoursToApplyPrevious = BigDecimal.ZERO;
341                     // If the hours before virtual day are less than or equal to
342                     // min hours, we have already applied the time, so we don't
343                     // set hoursToApplyPrevious
344 					if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) {
345 						// we need to apply these hours.
346 						hoursToApplyPrevious = hoursBeforeVirtualDay;
347 					}
348 
349 
350 					//  Current Day
351 
352 					TimeBlock previous = null; // Previous Time Block
353 					List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to.
354                     List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them.
355 					// Iterate over sorted list, checking time boundaries vs Shift Intervals.
356 					long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay);
357 
358                     boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks.
359                     if (!dayIsRuleActive(currentDay, rule)) {
360                         if (dayIsRuleActive(currentDay.minusDays(1), rule)) {
361                             previousDayOnly = true;
362                         } else {
363                             // Nothing to see here, move to next rule.
364                             continue;
365                         }
366 
367                     }
368 
369 					/*
370 					 * We will touch each time block and accumulate time blocks that are applicable to
371 					 * the current rule we are on.
372 					 */
373 
374                     // These blocks are only used for detail application
375                     // We don't want to pass along the previous pay period,
376                     // because we don't want to modify the time blocks on that
377                     // period. If null is passed, time will be placed on the
378                     // first block of the first period if the previous period
379                     // block had influence.
380                     List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev);
381 
382 					for (TimeBlock current : ruleTimeBlocksCurr) {
383 						if (!timeBlockHasEarnCode(fromEarnGroup, current)) {
384                             // TODO: WorkSchedule considerations somewhere in here?
385                             continue;
386                         }
387 
388 						Interval blockInterval = new Interval(current.getBeginDateTime().withZone(zone), current.getEndDateTime().withZone(zone));
389 
390 						// Check both Intervals, since the time blocks could still
391 						// be applicable to the previous day.  These two intervals should
392 						// not have any overlap.
393 						if (previousDayShiftInterval.overlaps(shiftInterval)) {
394 							LOG.error("Interval of greater than 24 hours created in the rules processing.");
395 							return;
396 //							throw new RuntimeException("Interval of greater than 24 hours created in the rules processing.");
397 						}
398 
399                         // This block of code handles cases where you have time
400                         // that spills to multiple days and a shift rule that
401                         // has a valid window on multiple consecutive days. Time
402                         // must be applied with the correct shift interval.
403 						Interval overlap = previousDayShiftInterval.overlap(blockInterval);
404                         evalInterval = previousDayShiftInterval;
405                         boolean overlapFromPreviousDay = true;
406 						if (overlap == null) {
407                             if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) {
408                                 // we have hours from previous day, and the shift
409                                 // window is going to move to current day.
410                                 // Need to apply this now, and move window forward
411                                 // for current time block.
412                                 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
413                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
414                                 accumulatedMillis = 0L; // reset accumulated hours..
415                                 hoursToApply = BigDecimal.ZERO;
416                                 hoursToApplyPrevious = BigDecimal.ZERO;
417                             }
418 
419                             // Because of our position in the loop, when we are at this point,
420                             // we know we've passed any previous day shift intervals, so we can
421                             // determine if we should skip the current day based on the boolean
422                             // we set earlier.
423                             if (previousDayOnly) {
424                                 continue;
425                             }
426 
427 							overlap = shiftInterval.overlap(blockInterval);
428                             evalInterval = shiftInterval;
429                             overlapFromPreviousDay = false;
430                         }
431 
432                         // Time bucketing and application as normal:
433                         //
434 						if (overlap != null) {
435 							// There IS overlap.
436 							if (previous != null) {
437 								// check if the evalInterval we are on covers the previous time block
438 								// if not, it means we need to apply the accumulated shift hours and start fresh on a new  interval								
439 								Interval previousBlockInterval = new Interval(previous.getBeginDateTime().withZone(zone), previous.getEndDateTime().withZone(zone));
440 								if(evalInterval.overlaps(previousBlockInterval)) {
441 									// only check max gap if max gap of rule is not 0
442 									if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) {
443 										BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
444 	                                    this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
445 	                                    accumulatedMillis = 0L; // reset accumulated hours..
446 										hoursToApply = BigDecimal.ZERO;
447 										hoursToApplyPrevious = BigDecimal.ZERO;
448 									} else {
449                                     //We really need a list of the shift intervals here as overlap can happen more than one
450                                     //per day
451                                         List<Interval> overlapIntervals = getOverlappingIntervals(blockInterval,rule);
452                                         for(Interval overlapWithShift : overlapIntervals){
453                                             long millis = overlapWithShift.toDurationMillis();
454                                             accumulatedMillis  += millis;
455                                             hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
456                                         }
457 									}
458 								} else {
459 									// rules from different days apply to time block on this day 
460 									// finish applying accumulated hours to the previous block
461 									BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
462 									this.applyAccumulatedWrapper(accumHours, previousDayShiftInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
463 									// start fresh with this block which has a new 
464 									long millis = overlap.toDurationMillis();
465 									accumulatedMillis  = millis;
466 									hoursToApply = TKUtils.convertMillisToHours(millis);
467 								}
468 							} else {								
469 								// get the date of previousDayShiftInterval, check is that day is active for the rule, if not, then don't accumulate the hours
470 								boolean previousDayActive = dayIsRuleActive(previousDayShiftInterval.getStart(), rule);
471 								if(!previousDayActive && overlapFromPreviousDay) {
472 									continue;
473 								} else {
474 									// Overlap shift at first time block.
475                                     List<Interval> overlapIntervals = getOverlappingIntervals(blockInterval,rule);
476                                     for(Interval overlapWithShift : overlapIntervals){
477                                         long millis = overlapWithShift.toDurationMillis();
478                                         accumulatedMillis  += millis;
479                                         hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
480                                     }
481 								}
482 							}
483 							accumulatedBlocks.add(current);
484                             accumulatedBlockIntervals.add(blockInterval);
485 							previous = current; // current can still apply to next.
486 						} else {
487 							// No Overlap / Outside of Rule
488 							if (previous != null) {
489 								BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
490                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
491 								accumulatedMillis = 0L; // reset accumulated hours..
492 								hoursToApply = BigDecimal.ZERO;
493 								hoursToApplyPrevious = BigDecimal.ZERO;
494 							}
495 						}
496 
497 					}
498 
499 					// All time blocks are iterated over, check for remainders.
500 					// Check containers for time, and apply if needed.
501 					BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
502                     this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
503                 }
504 			}
505 			// 	Keep track of previous as we move day by day.
506 			jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks;
507             previousPayPeriodPrevDay = false;
508 		}
509 
510 	}
511 
512     @Override
513     public List<ShiftDifferentialRule> getShiftDifferentialRules(String userPrincipalId, String location, String hrSalGroup, String payGrade, LocalDate fromEffdt, LocalDate toEffdt, String active, String showHist) {
514     	List<ShiftDifferentialRule> results = new ArrayList<ShiftDifferentialRule>();
515         
516     	List<ShiftDifferentialRule> shiftDifferentialRuleObjs = shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist);
517     
518     	for (ShiftDifferentialRule shiftDifferentialRuleObj : shiftDifferentialRuleObjs) {
519         	Map<String, String> roleQualification = new HashMap<String, String>();
520         	roleQualification.put(KimConstants.AttributeConstants.PRINCIPAL_ID, userPrincipalId);
521         	roleQualification.put(KPMERoleMemberAttribute.LOCATION.getRoleMemberAttributeName(), shiftDifferentialRuleObj.getLocation());
522         	
523         	Map<String, String> permissionDetails = new HashMap<String, String>();
524         	permissionDetails.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, KRADServiceLocatorWeb.getDocumentDictionaryService().getMaintenanceDocumentTypeName(ShiftDifferentialRule.class));
525         	
526         	if (!KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KPMENamespace.KPME_WKFLW.getNamespaceCode(),
527     				KPMEPermissionTemplate.VIEW_KPME_RECORD.getPermissionTemplateName(), permissionDetails)
528     		  || KimApiServiceLocator.getPermissionService().isAuthorizedByTemplate(userPrincipalId, KPMENamespace.KPME_WKFLW.getNamespaceCode(),
529     				  KPMEPermissionTemplate.VIEW_KPME_RECORD.getPermissionTemplateName(), permissionDetails, roleQualification)) {
530         		results.add(shiftDifferentialRuleObj);
531         	}
532     	}
533     	
534     	return results;
535     }
536 
537     private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) {
538         List<TimeBlock> filtered;
539 
540         if (blocks == null || blocks.size() == 0)
541             filtered = null;
542         else {
543             filtered = new ArrayList<TimeBlock>();
544             for (TimeBlock b : blocks) {
545                 if (timeBlockHasEarnCode(fromEarnGroup, b))
546                     filtered.add(b);
547             }
548         }
549 
550         return filtered;
551     }
552 
553     private void applyAccumulatedWrapper(BigDecimal accumHours,
554                                          Interval evalInterval,
555                                          List<Interval>accumulatedBlockIntervals,
556                                          List<TimeBlock>accumulatedBlocks,
557                                          List<TimeBlock> previousBlocks,
558                                          BigDecimal hoursToApplyPrevious,
559                                          BigDecimal hoursToApply,
560                                          ShiftDifferentialRule rule) {
561         if (accumHours.compareTo(rule.getMinHours()) >= 0) {
562             this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode(), rule);
563         }
564         accumulatedBlocks.clear();
565         accumulatedBlockIntervals.clear();
566     }
567 
568 	private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
569 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
570 			public int compare(TimeBlock tb1, TimeBlock tb2) {
571 				if (tb1 != null && tb2 != null)
572 					return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
573 				return 0;
574 			}
575 		});
576 	}
577 
578 	private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
579 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
580 			public int compare(TimeBlock tb1, TimeBlock tb2) {
581 				if (tb1 != null && tb2 != null)
582 					return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
583 				return 0;
584 			}
585 		});
586 	}
587 
588     /**
589      *
590      * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block.
591      * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation.
592      * @param blocks The blocks we are applying hours to.
593      * @param previousBlocks If present, this is the list of time blocks from a previous "day", on which the initial hours (from previous day) should be placed.
594      * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL)
595      * @param hours hours to apply
596      * @param earnCode what earn code to create time hour detail entry for.
597      */
598 	protected void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode, ShiftDifferentialRule rule) {
599         for (int i=0; i<blocks.size(); i++) {
600 			TimeBlock b = blocks.get(i);
601 
602             // Only apply initial hours to the first timeblock.
603 			if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) {
604                 // ONLY if they're on the same document ID, do we apply to previous,
605                 // otherwise we dump all on the current document.
606                 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) {
607                     for (TimeBlock pb : previousBlocks) {
608                         BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number
609                         initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours!
610                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply.
611                             break;
612 
613                         // Adjust hours on the block by the lunch sub hours, so we're not over applying.
614                         BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub));
615                         addPremiumTimeHourDetail(pb, hoursToApply, earnCode);
616                         initialHours = initialHours.subtract(hoursToApply, HrConstants.MATH_CONTEXT);
617                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0)
618                             break;
619                     }
620                 } else {
621 				    addPremiumTimeHourDetail(b, initialHours, earnCode);
622                 }
623             }
624 
625             BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number
626             hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours!
627 
628 			if (hours.compareTo(BigDecimal.ZERO) > 0) {
629                 Interval blockInterval = blockIntervals.get(i);
630                 List<Interval> overlapIntervals = getOverlappingIntervals(blockInterval, rule);
631                 for (Interval overlapInterval : overlapIntervals) {
632                     if (overlapInterval == null) {
633                         continue;
634                     }
635 
636                     long overlap = overlapInterval.toDurationMillis();
637                     BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule
638                     // Adjust this time block's hoursMax (below) by lunchSub to
639                     // make sure the time applied is the correct amount per block.
640                     BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub));
641 
642                     addPremiumTimeHourDetail(b, hoursToApply, earnCode);
643                     hours = hours.subtract(hoursToApply, HrConstants.MATH_CONTEXT);
644                 }
645 			}
646 		}
647 	}
648 
649     /**
650      * This is to allow for two periods to be evaluated by the rule we probably can incorporate
651      * this into core logic to allow for a List<Interval>
652      * This case fixed a rule that ran from 6p - 6a and 6p - 6a and it only evaluated the 6p-6a of the start day and
653      * not the previous day or a timeblock that spanned a rule from a previous day to current day then a new
654      * rule started on the same day and ends next day
655      * @param timeBlockOverlap
656      * @param rule
657      * @return
658      */
659     List<Interval> getOverlappingIntervals(Interval timeBlockOverlap, ShiftDifferentialRule rule){
660         List<Interval> overlappingIntervals = new ArrayList<Interval>();
661 
662         DateTimeZone zone = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
663 
664         //See if a shift interval starts on the previous day and has an overlap into the next day
665         DateTime previousDay = timeBlockOverlap.getStart().minusDays(1);
666         if(dayIsRuleActive(previousDay,rule)){
667             LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
668             LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
669 
670             DateTime shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getStart());
671             if (ruleEnd.isAfter(ruleStart)) {
672                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd().minusDays(1));
673             }
674             DateTime shiftStart = ruleStart.toDateTime(previousDay);
675             Interval shiftInterval = new Interval(shiftStart,shiftEnd);
676             Interval overlapInterval = shiftInterval.overlap(timeBlockOverlap);
677             if(overlapInterval != null){
678                 overlappingIntervals.add(overlapInterval);
679             }
680         }
681 
682         DateTime currentDay = timeBlockOverlap.getStart();
683         if(dayIsRuleActive(currentDay,rule)){
684             LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
685             LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
686 
687             DateTime shiftEnd = null;
688             if(ruleEnd.isBefore(ruleStart) || ruleEnd.isEqual(ruleStart)){
689                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd().plusDays(1));
690             }  else {
691                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd());
692             }
693             DateTime shiftStart = ruleStart.toDateTime(currentDay);
694             Interval shiftInterval = new Interval(shiftStart,shiftEnd);
695             Interval overlapInterval = shiftInterval.overlap(timeBlockOverlap);
696             if(overlapInterval != null){
697                 overlappingIntervals.add(overlapInterval);
698             }
699         }
700 
701         return overlappingIntervals;
702 
703     }
704 
705 	void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) {
706 		List<TimeHourDetail> details = block.getTimeHourDetails();
707 		TimeHourDetail premium = new TimeHourDetail();
708 		premium.setHours(hours);
709 		premium.setEarnCode(earnCode);
710 		premium.setTkTimeBlockId(block.getTkTimeBlockId());
711 		details.add(premium);
712 	}
713 
714 	/**
715 	 * Does the difference between the previous time blocks clock out time and the
716 	 * current time blocks clock in time exceed the max gap. max gap is in minutes
717 	 *
718 	 * @param previous
719 	 * @param current
720 	 * @param maxGap
721 	 * @return
722 	 */
723 	boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
724 		long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
725 		BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference);
726 
727 		return (gapMinutes.compareTo(maxGap) > 0);
728 	}
729 
730 	public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) {
731 		this.shiftDifferentialRuleDao = shiftDifferentialRuleDao;
732 	}
733 
734 	@Override
735 	public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) {
736 		return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId);
737 	}
738 
739 	@Override
740 	public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, LocalDate asOfDate) {
741 		List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>();
742 
743 		// location, sal group, pay grade, calendar
744 	    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
745 
746 		// location, sal group, *, calendar
747 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate));
748 
749 		// location, *, pay grade, calendar
750 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate));
751 
752 		// location, *, *, calendar
753 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate));
754 
755 		// *, sal group, pay grade, calendar
756 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
757 
758 		// *, sal group, *, calendar
759 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate));
760 
761 		// *, *, pay grade, calendar
762 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate));
763 
764 		// *, *, *, calendar
765 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate));
766 
767 		// location, sal group, pay grade, *
768 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, "%", asOfDate));
769 
770 		// location, sal group, *, *
771 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", "%", asOfDate));
772 
773 		// location, *, pay grade, *
774 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, "%", asOfDate));
775 		
776 		// location, *, *, *
777 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", "%", asOfDate));
778 
779 		// *, sal group, pay grade, *
780 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, "%", asOfDate));
781 
782 		// *, sal group, *, *
783 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", "%", asOfDate));
784 
785 		// *, *, pay grade, *
786 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, "%", asOfDate));
787 
788 		// *, *, *, *
789 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", "%", asOfDate));
790 
791 		return sdrs;
792 	}
793 
794 	private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) {
795 		boolean active = false;
796 
797 		switch (currentDate.getDayOfWeek()) {
798 		case DateTimeConstants.MONDAY:
799 			active = sdr.isMonday();
800 			break;
801 		case DateTimeConstants.TUESDAY:
802 			active = sdr.isTuesday();
803 			break;
804 		case DateTimeConstants.WEDNESDAY:
805 			active = sdr.isWednesday();
806 			break;
807 		case DateTimeConstants.THURSDAY:
808 			active = sdr.isThursday();
809 			break;
810 		case DateTimeConstants.FRIDAY:
811 			active = sdr.isFriday();
812 			break;
813 		case DateTimeConstants.SATURDAY:
814 			active = sdr.isSaturday();
815 			break;
816 		case DateTimeConstants.SUNDAY:
817 			active = sdr.isSunday();
818 			break;
819 		}
820 
821 		return active;
822 	}
823 
824 	@Override
825 	public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) {
826 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules);
827 	}
828 
829 	@Override
830 	public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) {
831 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule);
832 	}
833 
834 }