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 														BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis());
309 														hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs);
310 													}
311 
312 											} else {
313 												// Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded.
314 												break;
315 											}
316 
317 											previousBlockInterval = blockInterval;
318 
319 										}
320 									}
321 								} else {
322 									// DO NOTHING!
323 								}
324 							}
325 						}
326 					}
327 
328 					BigDecimal hoursToApply = BigDecimal.ZERO;
329 					BigDecimal hoursToApplyPrevious = BigDecimal.ZERO;
330                     // If the hours before virtual day are less than or equal to
331                     // min hours, we have already applied the time, so we don't
332                     // set hoursToApplyPrevious
333 					if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) {
334 						// we need to apply these hours.
335 						hoursToApplyPrevious = hoursBeforeVirtualDay;
336 					}
337 
338 
339 					//  Current Day
340 
341 					TimeBlock previous = null; // Previous Time Block
342 					List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to.
343                     List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them.
344 					// Iterate over sorted list, checking time boundaries vs Shift Intervals.
345 					long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay);
346 
347                     boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks.
348                     if (!dayIsRuleActive(currentDay, rule)) {
349                         if (dayIsRuleActive(currentDay.minusDays(1), rule)) {
350                             previousDayOnly = true;
351                         } else {
352                             // Nothing to see here, move to next rule.
353                             continue;
354                         }
355 
356                     }
357 
358 					/*
359 					 * We will touch each time block and accumulate time blocks that are applicable to
360 					 * the current rule we are on.
361 					 */
362 
363                     // These blocks are only used for detail application
364                     // We don't want to pass along the previous pay period,
365                     // because we don't want to modify the time blocks on that
366                     // period. If null is passed, time will be placed on the
367                     // first block of the first period if the previous period
368                     // block had influence.
369                     List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev);
370 
371 					for (TimeBlock current : ruleTimeBlocksCurr) {
372 						if (!timeBlockHasEarnCode(fromEarnGroup, current)) {
373                             // TODO: WorkSchedule considerations somewhere in here?
374                             continue;
375                         }
376 
377 						Interval blockInterval = new Interval(current.getBeginDateTime().withZone(zone), current.getEndDateTime().withZone(zone));
378 
379 						// Check both Intervals, since the time blocks could still
380 						// be applicable to the previous day.  These two intervals should
381 						// not have any overlap.
382 						if (previousDayShiftInterval.overlaps(shiftInterval)) {
383 							LOG.error("Interval of greater than 24 hours created in the rules processing.");
384 							return;
385 //							throw new RuntimeException("Interval of greater than 24 hours created in the rules processing.");
386 						}
387 
388                         // This block of code handles cases where you have time
389                         // that spills to multiple days and a shift rule that
390                         // has a valid window on multiple consecutive days. Time
391                         // must be applied with the correct shift interval.
392 						Interval overlap = previousDayShiftInterval.overlap(blockInterval);
393                         Interval overlapCurrentDay = shiftInterval.overlap(blockInterval);
394                         evalInterval = previousDayShiftInterval;
395                         boolean overlapFromPreviousDay = true;
396 						if (overlap == null) {
397                             if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) {
398                                 // we have hours from previous day, and the shift
399                                 // window is going to move to current day.
400                                 // Need to apply this now, and move window forward
401                                 // for current time block.
402                                 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
403                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
404                                 accumulatedMillis = 0L; // reset accumulated hours..
405                                 hoursToApply = BigDecimal.ZERO;
406                                 hoursToApplyPrevious = BigDecimal.ZERO;
407                             }
408 
409                             // Because of our position in the loop, when we are at this point,
410                             // we know we've passed any previous day shift intervals, so we can
411                             // determine if we should skip the current day based on the boolean
412                             // we set earlier.
413                             if (previousDayOnly) {
414                                 continue;
415                             }
416 
417 							overlap = shiftInterval.overlap(blockInterval);
418                             evalInterval = shiftInterval;
419                             overlapFromPreviousDay = false;
420                         }
421 
422 
423                         // Time bucketing and application as normal:
424                         //
425 						if (overlap != null) {
426 							// There IS overlap.
427 							if (previous != null) {
428 								// check if the evalInterval we are on covers the previous time block
429 								// if not, it means we need to apply the accumulated shift hours and start fresh on a new  interval								
430 								Interval previousBlockInterval = new Interval(previous.getBeginDateTime().withZone(zone), previous.getEndDateTime().withZone(zone));
431 								if(evalInterval.overlaps(previousBlockInterval)) {
432 									// only check max gap if max gap of rule is not 0
433 									if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) {
434 										BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
435 	                                    this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
436 	                                    accumulatedMillis = 0L; // reset accumulated hours..
437 										hoursToApply = BigDecimal.ZERO;
438 										hoursToApplyPrevious = BigDecimal.ZERO;
439 
440                                         //didn't hit max gap for previous, but we still need to check for possible next shift
441                                         Interval currentShiftOverlap = shiftInterval.overlap(blockInterval);
442                                         if (currentShiftOverlap != null) {
443                                             long millis = currentShiftOverlap.toDurationMillis();
444                                             accumulatedMillis  += millis;
445                                             hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
446                                         }
447 									} else {
448                                     //We really need a list of the shift intervals here as overlap can happen more than one
449                                     //per day
450                                         List<Interval> overlapIntervals = getOverlappingIntervals(blockInterval,rule);
451                                         for(Interval overlapWithShift : overlapIntervals){
452                                             long millis = overlapWithShift.toDurationMillis();
453                                             accumulatedMillis  += millis;
454                                             hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
455                                             }
456 									    }
457 								} else {
458 									// rules from different days apply to time block on this day 
459 									// finish applying accumulated hours to the previous block
460 									BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
461 									this.applyAccumulatedWrapper(accumHours, previousDayShiftInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
462 									// start fresh with this block which has a new 
463 									long millis = overlap.toDurationMillis();
464 									accumulatedMillis  = millis;
465 									hoursToApply = TKUtils.convertMillisToHours(millis);
466 								}
467 							} else {								
468 								// get the date of previousDayShiftInterval, check is that day is active for the rule, if not, then don't accumulate the hours
469 								boolean previousDayActive = dayIsRuleActive(previousDayShiftInterval.getStart(), rule);
470 								if(!previousDayActive && overlapFromPreviousDay) {
471 									continue;
472 								} else {
473 									// Overlap shift at first time block.
474                                     List<Interval> overlapIntervals = getOverlappingIntervals(blockInterval,rule);
475                                     for(Interval overlapWithShift : overlapIntervals){
476                                         long millis = overlapWithShift.toDurationMillis();
477                                         accumulatedMillis  += millis;
478                                         hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
479                                     }
480 								}
481 							}
482 							accumulatedBlocks.add(current);
483                             accumulatedBlockIntervals.add(blockInterval);
484 							previous = current; // current can still apply to next.
485 						} else {
486 							// No Overlap / Outside of Rule
487 							if (previous != null) {
488 								BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
489                                 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
490 								accumulatedMillis = 0L; // reset accumulated hours..
491 								hoursToApply = BigDecimal.ZERO;
492 								hoursToApplyPrevious = BigDecimal.ZERO;
493 							}
494 						}
495 
496 					}
497 
498 					// All time blocks are iterated over, check for remainders.
499 					// Check containers for time, and apply if needed.
500 					BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
501                     this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
502                 }
503 			}
504 			// 	Keep track of previous as we move day by day.
505 			jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks;
506             previousPayPeriodPrevDay = false;
507 		}
508 
509 	}
510 
511     @Override
512     public List<ShiftDifferentialRule> getShiftDifferentialRules(String userPrincipalId, String location, String hrSalGroup, String payGrade, LocalDate fromEffdt, LocalDate toEffdt, String active, String showHist) {
513     	List<ShiftDifferentialRule> results = new ArrayList<ShiftDifferentialRule>();
514         
515     	List<ShiftDifferentialRule> shiftDifferentialRuleObjs = shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist);
516     
517     	for (ShiftDifferentialRule shiftDifferentialRuleObj : shiftDifferentialRuleObjs) {
518         	Map<String, String> roleQualification = new HashMap<String, String>();
519         	roleQualification.put(KimConstants.AttributeConstants.PRINCIPAL_ID, userPrincipalId);
520         	roleQualification.put(KPMERoleMemberAttribute.LOCATION.getRoleMemberAttributeName(), shiftDifferentialRuleObj.getLocation());
521         	
522         	Map<String, String> permissionDetails = new HashMap<String, String>();
523         	permissionDetails.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, KRADServiceLocatorWeb.getDocumentDictionaryService().getMaintenanceDocumentTypeName(ShiftDifferentialRule.class));
524         	
525         	if (!KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KPMENamespace.KPME_WKFLW.getNamespaceCode(),
526     				KPMEPermissionTemplate.VIEW_KPME_RECORD.getPermissionTemplateName(), permissionDetails)
527     		  || KimApiServiceLocator.getPermissionService().isAuthorizedByTemplate(userPrincipalId, KPMENamespace.KPME_WKFLW.getNamespaceCode(),
528     				  KPMEPermissionTemplate.VIEW_KPME_RECORD.getPermissionTemplateName(), permissionDetails, roleQualification)) {
529         		results.add(shiftDifferentialRuleObj);
530         	}
531     	}
532     	
533     	return results;
534     }
535 
536     private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) {
537         List<TimeBlock> filtered;
538 
539         if (blocks == null || blocks.size() == 0)
540             filtered = null;
541         else {
542             filtered = new ArrayList<TimeBlock>();
543             for (TimeBlock b : blocks) {
544                 if (timeBlockHasEarnCode(fromEarnGroup, b))
545                     filtered.add(b);
546             }
547         }
548 
549         return filtered;
550     }
551 
552     private void applyAccumulatedWrapper(BigDecimal accumHours,
553                                          Interval evalInterval,
554                                          List<Interval>accumulatedBlockIntervals,
555                                          List<TimeBlock>accumulatedBlocks,
556                                          List<TimeBlock> previousBlocks,
557                                          BigDecimal hoursToApplyPrevious,
558                                          BigDecimal hoursToApply,
559                                          ShiftDifferentialRule rule) {
560         if (accumHours.compareTo(rule.getMinHours()) >= 0) {
561             this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode(), rule);
562         }
563         accumulatedBlocks.clear();
564         accumulatedBlockIntervals.clear();
565     }
566 
567 	private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
568 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
569 			public int compare(TimeBlock tb1, TimeBlock tb2) {
570 				if (tb1 != null && tb2 != null)
571 					return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
572 				return 0;
573 			}
574 		});
575 	}
576 
577 	private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
578 		Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
579 			public int compare(TimeBlock tb1, TimeBlock tb2) {
580 				if (tb1 != null && tb2 != null)
581 					return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
582 				return 0;
583 			}
584 		});
585 	}
586 
587     /**
588      *
589      * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block.
590      * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation.
591      * @param blocks The blocks we are applying hours to.
592      * @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.
593      * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL)
594      * @param hours hours to apply
595      * @param earnCode what earn code to create time hour detail entry for.
596      */
597 	protected void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode, ShiftDifferentialRule rule) {
598         Map<Interval, Long> nextGaps = new HashMap<Interval, Long>();
599         List<Interval> possibleShifts = new ArrayList<Interval>(3);
600         possibleShifts.add(new Interval(shift.getStart().minusDays(1), shift.getEnd().minusDays(1)));
601         possibleShifts.add(shift);
602         possibleShifts.add(new Interval(shift.getStart().plusDays(1), shift.getEnd().plusDays(1)));
603         Map<Interval, Long> shiftOverlapMillis = new HashMap<Interval, Long>();
604         Map<Interval, Interval> overlapToShift = new HashMap<Interval, Interval>();
605         List<Interval> allOverlaps = new ArrayList<Interval>();
606         for (Interval interval : blockIntervals) {
607             allOverlaps.addAll(getOverlappingIntervals(interval, rule));
608         }
609         if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0) {
610             for (int i = 0; i < allOverlaps.size(); i++) {
611                 Long gap = 0L;
612                 if (i+1 < allOverlaps.size()) {
613                     gap = allOverlaps.get(i+1).getStartMillis() - allOverlaps.get(i).getEndMillis();
614                 }
615                 nextGaps.put(allOverlaps.get(i), gap);
616             }
617         }
618         for (Interval overlap : allOverlaps) {
619             for (Interval possibleShift : possibleShifts) {
620                 if (possibleShift.overlaps(overlap)) {
621                     overlapToShift.put(overlap, possibleShift);
622                     if (!shiftOverlapMillis.containsKey(possibleShift)) {
623                         shiftOverlapMillis.put(possibleShift, possibleShift.overlap(overlap).toDurationMillis());
624                     } else {
625                         shiftOverlapMillis.put(possibleShift, (shiftOverlapMillis.get(possibleShift) + possibleShift.overlap(overlap).toDurationMillis()));
626                     }
627                     //shiftOverlapMillis.put
628                     //totalOverlapMillisInShift += shift.overlap(overlap).toDurationMillis();
629                 }
630             }
631         }
632 
633         for (int i=0; i<blocks.size(); i++) {
634 			TimeBlock b = blocks.get(i);
635 
636             // Only apply initial hours to the first timeblock.
637 			if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) {
638                 // ONLY if they're on the same document ID, do we apply to previous,
639                 // otherwise we dump all on the current document.
640                 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) {
641                     for (TimeBlock pb : previousBlocks) {
642                         BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number
643                         initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours!
644                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply.
645                             break;
646 
647                         // Adjust hours on the block by the lunch sub hours, so we're not over applying.
648                         BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub));
649                         addPremiumTimeHourDetail(pb, hoursToApply, earnCode);
650                         initialHours = initialHours.subtract(hoursToApply, HrConstants.MATH_CONTEXT);
651                         if (initialHours.compareTo(BigDecimal.ZERO) <= 0)
652                             break;
653                     }
654                 } else {
655 				    addPremiumTimeHourDetail(b, initialHours, earnCode);
656                 }
657             }
658 
659             BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number
660             hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours!
661 
662 			if (hours.compareTo(BigDecimal.ZERO) > 0) {
663                 Interval blockInterval = blockIntervals.get(i);
664                 List<Interval> overlapIntervals = getOverlappingIntervals(blockInterval, rule);
665                 BigDecimal allHoursToApply = BigDecimal.ZERO;
666                 for (Interval overlapInterval : overlapIntervals) {
667                     if (overlapInterval == null) {
668                         continue;
669                     }
670 
671                     long minMillis = TKUtils.convertHoursToMillis(rule.getMinHours());
672                     long overlap = overlapInterval.toDurationMillis();
673                     BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule
674                     // Adjust this time block's hoursMax (below) by lunchSub to
675                     // make sure the time applied is the correct amount per block.
676                     //if (overlap >= minMillis || overlapIntervals.size() == 1) {
677                     Interval overlapShift = overlapToShift.get(overlapInterval);
678                     Long totalOverlapMillisInShift = shiftOverlapMillis.get(overlapShift);
679                     if (totalOverlapMillisInShift >= minMillis
680                             || overlapIntervals.size() == 1) {
681                         BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub));
682                         allHoursToApply = allHoursToApply.add(hoursToApply);
683                     }
684                 }
685                 if (allHoursToApply.compareTo(BigDecimal.ZERO) > 0) {
686                     addPremiumTimeHourDetail(b, allHoursToApply, earnCode);
687                     hours = hours.subtract(allHoursToApply, HrConstants.MATH_CONTEXT);
688                 }
689 			}
690 		}
691 	}
692 
693     /*protected List<List<Interval>> getOverlappingIntervalsPerShift(Interval timeBlockOverlap, ShiftDifferentialRule rule) {
694         List<List<Interval>> overlapsPer  = new ArrayList<Interval>();
695 
696         DateTimeZone zone = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
697 
698         //See if a shift interval starts on the previous day and has an overlap into the next day
699         DateTime previousDay = timeBlockOverlap.getStart().minusDays(1);
700         if(dayIsRuleActive(previousDay,rule)){
701             LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
702             LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
703 
704             DateTime shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getStart());
705             if (ruleEnd.isAfter(ruleStart)) {
706                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd().minusDays(1));
707             }
708             DateTime shiftStart = ruleStart.toDateTime(previousDay);
709             Interval shiftInterval = new Interval(shiftStart,shiftEnd);
710             Interval overlapInterval = shiftInterval.overlap(timeBlockOverlap);
711             if(overlapInterval != null){
712 
713                 shiftIntervals.add(shiftInterval);
714             } else {
715                 Collections.
716             }
717         }
718 
719         DateTime currentDay = timeBlockOverlap.getStart();
720         if(dayIsRuleActive(currentDay,rule)){
721             LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
722             LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
723 
724             DateTime shiftEnd = null;
725             if(ruleEnd.isBefore(ruleStart) || ruleEnd.isEqual(ruleStart)){
726                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd().plusDays(1));
727             }  else {
728                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd());
729             }
730             DateTime shiftStart = ruleStart.toDateTime(currentDay);
731             Interval shiftInterval = new Interval(shiftStart,shiftEnd);
732             Interval overlapInterval = shiftInterval.overlap(timeBlockOverlap);
733             if(overlapInterval != null){
734                 shiftIntervals.add(shiftInterval);
735             }
736         }
737 
738         return shiftIntervals;
739     }
740     */
741 
742     /**
743      * This is to allow for two periods to be evaluated by the rule we probably can incorporate
744      * this into core logic to allow for a List<Interval>
745      * 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
746      * not the previous day or a timeblock that spanned a rule from a previous day to current day then a new
747      * rule started on the same day and ends next day
748      * @param timeBlockOverlap
749      * @param rule
750      * @return
751      */
752     protected List<Interval> getOverlappingIntervals(Interval timeBlockOverlap, ShiftDifferentialRule rule){
753         List<Interval> overlappingIntervals = new ArrayList<Interval>();
754 
755         DateTimeZone zone = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
756 
757         //See if a shift interval starts on the previous day and has an overlap into the next day
758         DateTime previousDay = timeBlockOverlap.getStart().minusDays(1);
759         if(dayIsRuleActive(previousDay,rule)){
760             LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
761             LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
762 
763             DateTime shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getStart());
764             if (ruleEnd.isAfter(ruleStart)) {
765                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd().minusDays(1));
766             }
767             DateTime shiftStart = ruleStart.toDateTime(previousDay);
768             Interval shiftInterval = new Interval(shiftStart,shiftEnd);
769             Interval overlapInterval = shiftInterval.overlap(timeBlockOverlap);
770             if(overlapInterval != null){
771                 overlappingIntervals.add(overlapInterval);
772             }
773         }
774 
775         DateTime currentDay = timeBlockOverlap.getStart();
776         if(dayIsRuleActive(currentDay,rule)){
777             LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
778             LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
779 
780             DateTime shiftEnd = null;
781             if(ruleEnd.isBefore(ruleStart) || ruleEnd.isEqual(ruleStart)){
782                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd().plusDays(1));
783             }  else {
784                 shiftEnd = ruleEnd.toDateTime(timeBlockOverlap.getEnd());
785             }
786             DateTime shiftStart = ruleStart.toDateTime(currentDay);
787             Interval shiftInterval = new Interval(shiftStart,shiftEnd);
788             Interval overlapInterval = shiftInterval.overlap(timeBlockOverlap);
789             if(overlapInterval != null){
790                 overlappingIntervals.add(overlapInterval);
791             }
792         }
793 
794         return overlappingIntervals;
795 
796     }
797 
798 	void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) {
799 		List<TimeHourDetail> details = block.getTimeHourDetails();
800 		TimeHourDetail premium = new TimeHourDetail();
801 		premium.setHours(hours);
802 		premium.setEarnCode(earnCode);
803 		premium.setTkTimeBlockId(block.getTkTimeBlockId());
804 		details.add(premium);
805 	}
806 
807 	/**
808 	 * Does the difference between the previous time blocks clock out time and the
809 	 * current time blocks clock in time exceed the max gap. max gap is in minutes
810 	 *
811 	 * @param previous
812 	 * @param current
813 	 * @param maxGap
814 	 * @return
815 	 */
816 	boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
817 		long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
818 		BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference);
819 
820 		return (gapMinutes.compareTo(maxGap) > 0);
821 	}
822 
823 	public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) {
824 		this.shiftDifferentialRuleDao = shiftDifferentialRuleDao;
825 	}
826 
827 	@Override
828 	public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) {
829 		return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId);
830 	}
831 
832 	@Override
833 	public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, LocalDate asOfDate) {
834 		List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>();
835 
836 		// location, sal group, pay grade, calendar
837 	    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
838 
839 		// location, sal group, *, calendar
840 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate));
841 
842 		// location, *, pay grade, calendar
843 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate));
844 
845 		// location, *, *, calendar
846 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate));
847 
848 		// *, sal group, pay grade, calendar
849 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
850 
851 		// *, sal group, *, calendar
852 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate));
853 
854 		// *, *, pay grade, calendar
855 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate));
856 
857 		// *, *, *, calendar
858 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate));
859 
860 		// location, sal group, pay grade, *
861 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, "%", asOfDate));
862 
863 		// location, sal group, *, *
864 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", "%", asOfDate));
865 
866 		// location, *, pay grade, *
867 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, "%", asOfDate));
868 		
869 		// location, *, *, *
870 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", "%", asOfDate));
871 
872 		// *, sal group, pay grade, *
873 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, "%", asOfDate));
874 
875 		// *, sal group, *, *
876 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", "%", asOfDate));
877 
878 		// *, *, pay grade, *
879 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, "%", asOfDate));
880 
881 		// *, *, *, *
882 		sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", "%", asOfDate));
883 
884 		return sdrs;
885 	}
886 
887 	private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) {
888 		boolean active = false;
889 
890 		switch (currentDate.getDayOfWeek()) {
891 		case DateTimeConstants.MONDAY:
892 			active = sdr.isMonday();
893 			break;
894 		case DateTimeConstants.TUESDAY:
895 			active = sdr.isTuesday();
896 			break;
897 		case DateTimeConstants.WEDNESDAY:
898 			active = sdr.isWednesday();
899 			break;
900 		case DateTimeConstants.THURSDAY:
901 			active = sdr.isThursday();
902 			break;
903 		case DateTimeConstants.FRIDAY:
904 			active = sdr.isFriday();
905 			break;
906 		case DateTimeConstants.SATURDAY:
907 			active = sdr.isSaturday();
908 			break;
909 		case DateTimeConstants.SUNDAY:
910 			active = sdr.isSunday();
911 			break;
912 		}
913 
914 		return active;
915 	}
916 
917 	@Override
918 	public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) {
919 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules);
920 	}
921 
922 	@Override
923 	public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) {
924 		shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule);
925 	}
926 
927 }