001    /**
002     * Copyright 2004-2013 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.hr.time.shiftdiff.rule.service;
017    
018    import org.apache.log4j.Logger;
019    import org.joda.time.*;
020    import org.kuali.hr.job.Job;
021    import org.kuali.hr.time.calendar.CalendarEntries;
022    import org.kuali.hr.time.principal.PrincipalHRAttributes;
023    import org.kuali.hr.time.service.base.TkServiceLocator;
024    import org.kuali.hr.time.shiftdiff.rule.ShiftDifferentialRule;
025    import org.kuali.hr.time.shiftdiff.rule.dao.ShiftDifferentialRuleDao;
026    import org.kuali.hr.time.timeblock.TimeBlock;
027    import org.kuali.hr.time.timeblock.TimeHourDetail;
028    import org.kuali.hr.time.timesheet.TimesheetDocument;
029    import org.kuali.hr.time.util.TKUtils;
030    import org.kuali.hr.time.util.TkConstants;
031    import org.kuali.hr.time.util.TkTimeBlockAggregate;
032    import org.kuali.hr.time.workflow.TimesheetDocumentHeader;
033    
034    import java.math.BigDecimal;
035    import java.sql.Date;
036    import java.util.*;
037    
038    
039    public class ShiftDifferentialRuleServiceImpl implements ShiftDifferentialRuleService {
040    
041            @SuppressWarnings("unused")
042            private static final Logger LOG = Logger.getLogger(ShiftDifferentialRuleServiceImpl.class);
043            /**
044             * The maximum allowable time between timeblocks before we consider them to
045             * be day-boundary single time blocks.
046             */
047            private ShiftDifferentialRuleDao shiftDifferentialRuleDao = null;
048    
049            private Map<Long,List<ShiftDifferentialRule>> getJobNumberToShiftRuleMap(TimesheetDocument timesheetDocument) {
050                    Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = new HashMap<Long,List<ShiftDifferentialRule>>();
051                    PrincipalHRAttributes principalCal = TkServiceLocator.getPrincipalHRAttributeService().getPrincipalCalendar(timesheetDocument.getPrincipalId(),timesheetDocument.getCalendarEntry().getEndPeriodDate());
052    
053                    for (Job job : timesheetDocument.getJobs()) {
054                            List<ShiftDifferentialRule> shiftDifferentialRules = getShiftDifferentalRules(job.getLocation(),job.getHrSalGroup(),job.getPayGrade(),principalCal.getPayCalendar(),
055                                            TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getEndPeriodDateTime()));
056                            if (shiftDifferentialRules.size() > 0)
057                                    jobNumberToShifts.put(job.getJobNumber(), shiftDifferentialRules);
058                    }
059    
060                    return jobNumberToShifts;
061            }
062    
063            private Map<Long,List<TimeBlock>> getPreviousPayPeriodLastDayJobToTimeBlockMap(TimesheetDocument timesheetDocument, Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts) {
064                    Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay = null;
065    
066                    // Get the last day of the last week of the previous pay period.
067                    // This is the only day that can have impact on the current day.
068                    List<TimeBlock> prevBlocks = TkServiceLocator.getTimesheetService().getPrevDocumentTimeBlocks(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDate());
069                    if (prevBlocks.size() > 0) {
070                            TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getBeginDate());
071                            if (prevTdh != null) {
072                                    CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getEndDate(), TkConstants.PAY_CALENDAR_TYPE);
073                                    TkTimeBlockAggregate prevTimeAggregate = new TkTimeBlockAggregate(prevBlocks, prevPayCalendarEntry, prevPayCalendarEntry.getCalendarObj(), true);
074                                    List<List<TimeBlock>> dayBlocks = prevTimeAggregate.getDayTimeBlockList();
075                                    List<TimeBlock> previousPeriodLastDayBlocks = dayBlocks.get(dayBlocks.size() - 1);
076                                    // Set back to null if there is nothing in the list.
077                                    if (previousPeriodLastDayBlocks.size() > 0) {
078                                            jobNumberToTimeBlocksPreviousDay = new HashMap<Long, List<TimeBlock>>();
079    
080                                            for (TimeBlock block : previousPeriodLastDayBlocks) {
081                                                    // Job Number to TimeBlock for Last Day of Previous Time
082                                                    // Period
083                                                    Long jobNumber = block.getJobNumber();
084                                                    if (jobNumberToShifts.containsKey(jobNumber)) {
085                                                            // we have a useful timeblock.
086                                                            List<TimeBlock> jblist = jobNumberToTimeBlocksPreviousDay.get(jobNumber);
087                                                            if (jblist == null) {
088                                                                    jblist = new ArrayList<TimeBlock>();
089                                                                    jobNumberToTimeBlocksPreviousDay.put(jobNumber, jblist);
090                                                            }
091                                                            jblist.add(block);
092                                                    }
093                                            }
094                                    }
095                            }
096                    }
097    
098                    return jobNumberToTimeBlocksPreviousDay;
099            }
100    
101            private boolean timeBlockHasEarnCode(Set<String> earnCodes, TimeBlock block) {
102                    boolean present = false;
103    
104                    if (block != null && earnCodes != null)
105                            present = earnCodes.contains(block.getEarnCode());
106    
107                    return present;
108            }
109    
110        /**
111         * Returns a BigDecimal representing the sum of all of the negative time
112         * hour detail types. In this case, only LUN is considered. This can be
113         * modified to add other considerations.
114         *
115         * @param block The Timeblock to inspect.
116         *
117         * @return A big decimal.
118         */
119        private BigDecimal negativeTimeHourDetailSum(TimeBlock block) {
120            BigDecimal sum = BigDecimal.ZERO;
121    
122            if (block != null) {
123                List<TimeHourDetail> details = block.getTimeHourDetails();
124                for (TimeHourDetail detail : details) {
125                    if (detail.getEarnCode().equals(TkConstants.LUNCH_EARN_CODE)) {
126                        sum = sum.add(detail.getHours());
127                    }
128                }
129            }
130    
131            return sum;
132        }
133    
134            @Override
135            public void processShiftDifferentialRules(TimesheetDocument timesheetDocument, TkTimeBlockAggregate aggregate) {
136            DateTimeZone zone = TkServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
137                    List<List<TimeBlock>> blockDays = aggregate.getDayTimeBlockList();
138                    DateTime periodStartDateTime = timesheetDocument.getCalendarEntry().getBeginLocalDateTime().toDateTime(zone);
139                    Map<Long,List<ShiftDifferentialRule>> jobNumberToShifts = getJobNumberToShiftRuleMap(timesheetDocument);
140    
141    
142            // If there are no shift differential rules, we have an early exit.
143                    if (jobNumberToShifts.isEmpty()) {
144                            return;
145                    }
146    
147                    // Get the last day of the previous pay period. We need this to determine
148                    // if there are hours from the previous pay period that will effect the
149                    // shift rule on the first day of the currently-being-processed pay period.
150                    //
151            // Will be set to null if not applicable.
152            boolean previousPayPeriodPrevDay = true;
153                    Map<Long, List<TimeBlock>> jobNumberToTimeBlocksPreviousDay =
154                    getPreviousPayPeriodLastDayJobToTimeBlockMap(timesheetDocument, jobNumberToShifts);
155    
156                    // We are going to look at the time blocks grouped by Days.
157            //
158            // This is a very large outer loop.
159                    for (int pos = 0; pos < blockDays.size(); pos++) {
160                            List<TimeBlock> blocks = blockDays.get(pos); // Timeblocks for this day.
161                            if (blocks.isEmpty())
162                                    continue; // No Time blocks, no worries.
163    
164                            DateTime currentDay = periodStartDateTime.plusDays(pos);
165                            Interval virtualDay = new Interval(currentDay, currentDay.plusHours(24));
166    
167                            // Builds our JobNumber to TimeBlock for Current Day List.
168                //
169                // Shift Differential Rules are also grouped by Job number, this
170                // provides a quick way to do the lookup / reference.
171                // We don't need every time block, only the ones that will be
172                // applicable to the shift rules.
173                            Map<Long, List<TimeBlock>> jobNumberToTimeBlocks = new HashMap<Long,List<TimeBlock>>();
174                            for (TimeBlock block : blocks) {
175                                    Long jobNumber = block.getJobNumber();
176                                    if (jobNumberToShifts.containsKey(jobNumber)) {
177                                            List<TimeBlock> jblist = jobNumberToTimeBlocks.get(jobNumber);
178                                            if (jblist == null) {
179                                                    jblist = new ArrayList<TimeBlock>();
180                                                    jobNumberToTimeBlocks.put(jobNumber, jblist);
181                                            }
182                                            jblist.add(block);
183                                    }
184                            }
185    
186    
187                // Large Outer Loop to look at applying the Shift Rules based on
188                // the current JobNumber.
189                //
190                            // This loop will handle previous day boundary time as well as the
191                // current day.
192                //
193                // There is room for refactoring here!
194                            for (Map.Entry<Long, List<ShiftDifferentialRule>> entry : jobNumberToShifts.entrySet()) {
195                                    List<ShiftDifferentialRule> shiftDifferentialRules = entry.getValue();
196                                    // Obtain and sort our previous and current time blocks.
197                                    List<TimeBlock> ruleTimeBlocksPrev = null;
198                                    List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(entry.getKey());
199                                    if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) {
200                                            if (jobNumberToTimeBlocksPreviousDay != null)
201                                                    ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(entry.getKey());
202                                            if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0)
203                                                    this.sortTimeBlocksInverse(ruleTimeBlocksPrev);
204                                            this.sortTimeBlocksNatural(ruleTimeBlocksCurr);
205                                    } else {
206                                            // Skip to next job, there is nothing for this job
207                                            // on this day, and because of this we don't care
208                                            // about the previous day either.
209                                            continue;
210                                    }
211    
212                                    for (ShiftDifferentialRule rule : shiftDifferentialRules) {
213                                            Set<String> fromEarnGroup = TkServiceLocator.getEarnCodeGroupService().getEarnCodeListForEarnCodeGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getCalendarEntry().getBeginPeriodDateTime()));
214    
215                        LocalTime ruleStart = new LocalTime(rule.getBeginTime(), zone);
216                        LocalTime ruleEnd = new LocalTime(rule.getEndTime(), zone);
217    
218    
219                                            DateTime shiftEnd = ruleEnd.toDateTime(currentDay);
220                                            DateTime shiftStart = ruleStart.toDateTime(currentDay);
221    
222                                            if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) {
223                                                    shiftEnd = shiftEnd.plusDays(1);
224                        }
225                                            Interval shiftInterval = new Interval(shiftStart, shiftEnd);
226    
227                                            // Set up buckets to handle previous days time accumulations
228                                            BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO;
229    
230                                            // Check current day first block to see if start time gap from virtual day start is greater than max gap
231                                            // if so, we can skip the previous day checks.
232                                            TimeBlock firstBlockOfCurrentDay = null;
233                                            for (TimeBlock b : ruleTimeBlocksCurr) {
234                                                    if (timeBlockHasEarnCode(fromEarnGroup, b)) {
235                                                            firstBlockOfCurrentDay = b;
236                                                            break;
237                                                    }
238                                            }
239    
240                                            // Previous Day :: We have prior block container of nonzero size, and the previous day is active.
241                                            Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1));
242    
243                        // Blank initialization pointer for picking which interval to pass to applyPremium()
244                        Interval evalInterval = null;
245                                            if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) {
246                                                    // Simple heuristic to see if we even need to worry about
247                                                    // the Shift rule for this set of data.
248                                                    if (shiftEnd.isAfter(virtualDay.getEnd())) {
249                                                            // Compare first block of previous day with first block of current day for max gaptitude.
250                                                            TimeBlock firstBlockOfPreviousDay = null;
251                                                            for (TimeBlock b : ruleTimeBlocksPrev) {
252                                                                    if (timeBlockHasEarnCode(fromEarnGroup, b)) {
253                                                                            firstBlockOfPreviousDay = b;
254                                                                            break;
255                                                                    }
256                                                            }
257                                                            // Only if we actually have at least one block.
258                                // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day.
259                                // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which
260                                // blocks are valid.
261                                                            if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) {
262                                                                    Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone));
263                                                                    Duration blockGapDuration = previousBlockInterval.toDuration();
264                                                                    BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
265                                                                    // if maxGap is 0, ignore gaps and assign shift to time blocks within the hours
266                                                                    if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
267                                                                            // If we are here, we know we have at least one valid time block to pull some hours forward from.
268    
269    
270                                                                            // These are inversely sorted.
271                                                                            for (int i=0; i<ruleTimeBlocksPrev.size(); i++) {
272                                                                                    TimeBlock b = ruleTimeBlocksPrev.get(i);
273                                                                                    if (timeBlockHasEarnCode(fromEarnGroup, b)) {
274                                                                                            Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone));
275    
276                                                                                            // Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks.
277                                                                                            if (previousBlockInterval != null) {
278                                                                                                    blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart());
279                                                                                                    bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis());
280                                                                                            }
281    
282                                                                                            // Check Gap, if good, sum hours, if maxGap is 0, ignore gaps
283                                                                                            if (rule.getMaxGap().compareTo(BigDecimal.ZERO) == 0 || bgdHours.compareTo(rule.getMaxGap()) <= 0) {
284                                                                                                    // Calculate Overlap and add it to hours before virtual day bucket.
285                                                                                                    if (blockInterval.overlaps(previousDayShiftInterval)) {
286                                                                                                            BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis());
287                                                                                                            hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs);
288                                                                                                    }
289    
290                                                                                            } else {
291                                                                                                    // Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded.
292                                                                                                    break;
293                                                                                            }
294    
295                                                                                            previousBlockInterval = blockInterval;
296    
297                                                                                    }
298                                                                            }
299                                                                    } else {
300                                                                            // DO NOTHING!
301                                                                    }
302                                                            }
303                                                    }
304                                            }
305    
306                                            BigDecimal hoursToApply = BigDecimal.ZERO;
307                                            BigDecimal hoursToApplyPrevious = BigDecimal.ZERO;
308                        // If the hours before virtual day are less than or equal to
309                        // min hours, we have already applied the time, so we don't
310                        // set hoursToApplyPrevious
311                                            if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) {
312                                                    // we need to apply these hours.
313                                                    hoursToApplyPrevious = hoursBeforeVirtualDay;
314                                            }
315    
316    
317                                            //  Current Day
318    
319                                            TimeBlock previous = null; // Previous Time Block
320                                            List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to.
321                        List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them.
322                                            // Iterate over sorted list, checking time boundaries vs Shift Intervals.
323                                            long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay);
324    
325                        boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks.
326                        if (!dayIsRuleActive(currentDay, rule)) {
327                            if (dayIsRuleActive(currentDay.minusDays(1), rule)) {
328                                previousDayOnly = true;
329                            } else {
330                                // Nothing to see here, move to next rule.
331                                continue;
332                            }
333    
334                        }
335    
336                                            /*
337                                             * We will touch each time block and accumulate time blocks that are applicable to
338                                             * the current rule we are on.
339                                             */
340    
341                        // These blocks are only used for detail application
342                        // We don't want to pass along the previous pay period,
343                        // because we don't want to modify the time blocks on that
344                        // period. If null is passed, time will be placed on the
345                        // first block of the first period if the previous period
346                        // block had influence.
347                        List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev);
348    
349                                            for (TimeBlock current : ruleTimeBlocksCurr) {
350                                                    if (!timeBlockHasEarnCode(fromEarnGroup, current)) {
351                                // TODO: WorkSchedule considerations somewhere in here?
352                                continue;
353                            }
354    
355                                                    Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone));
356    
357                                                    // Check both Intervals, since the time blocks could still
358                                                    // be applicable to the previous day.  These two intervals should
359                                                    // not have any overlap.
360                                                    if (previousDayShiftInterval.overlaps(shiftInterval))
361                                                            throw new RuntimeException("Interval of greater than 24 hours created in the rules processing.");
362    
363                            // This block of code handles cases where you have time
364                            // that spills to multiple days and a shift rule that
365                            // has a valid window on multiple consecutive days. Time
366                            // must be applied with the correct shift interval.
367                                                    Interval overlap = previousDayShiftInterval.overlap(blockInterval);
368                            evalInterval = previousDayShiftInterval;
369                                                    if (overlap == null) {
370                                if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) {
371                                    // we have hours from previous day, and the shift
372                                    // window is going to move to current day.
373                                    // Need to apply this now, and move window forward
374                                    // for current time block.
375                                    BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
376                                    this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
377                                    accumulatedMillis = 0L; // reset accumulated hours..
378                                    hoursToApply = BigDecimal.ZERO;
379                                    hoursToApplyPrevious = BigDecimal.ZERO;
380                                }
381    
382                                // Because of our position in the loop, when we are at this point,
383                                // we know we've passed any previous day shift intervals, so we can
384                                // determine if we should skip the current day based on the boolean
385                                // we set earlier.
386                                if (previousDayOnly) {
387                                    continue;
388                                }
389    
390                                                            overlap = shiftInterval.overlap(blockInterval);
391                                evalInterval = shiftInterval;
392                            }
393    
394                            // Time bucketing and application as normal:
395                            //
396                                                    if (overlap != null) {
397                                                            // There IS overlap.
398                                                            if (previous != null) {
399                                                                    // only check max gap if max gap of rule is not 0
400                                                                    if (rule.getMaxGap().compareTo(BigDecimal.ZERO) != 0 && exceedsMaxGap(previous, current, rule.getMaxGap())) {
401                                                                            BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
402                                        this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
403                                        accumulatedMillis = 0L; // reset accumulated hours..
404                                                                            hoursToApply = BigDecimal.ZERO;
405                                                                            hoursToApplyPrevious = BigDecimal.ZERO;
406                                                                    } else {
407                                                                            long millis = overlap.toDurationMillis();
408                                                                            accumulatedMillis  += millis;
409                                                                            hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
410                                                                    }
411                                                            } else {
412                                                                    // Overlap shift at first time block.
413                                                                    long millis = overlap.toDurationMillis();
414                                                                    accumulatedMillis  += millis;
415                                                                    hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis));
416                                                            }
417                                                            accumulatedBlocks.add(current);
418                                accumulatedBlockIntervals.add(blockInterval);
419                                                            previous = current; // current can still apply to next.
420                                                    } else {
421                                                            // No Overlap / Outside of Rule
422                                                            if (previous != null) {
423                                                                    BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
424                                    this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
425                                                                    accumulatedMillis = 0L; // reset accumulated hours..
426                                                                    hoursToApply = BigDecimal.ZERO;
427                                                                    hoursToApplyPrevious = BigDecimal.ZERO;
428                                                            }
429                                                    }
430    
431                                            }
432    
433                                            // All time blocks are iterated over, check for remainders.
434                                            // Check containers for time, and apply if needed.
435                                            BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis);
436                        this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule);
437                    }
438                            }
439                            //      Keep track of previous as we move day by day.
440                            jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks;
441                previousPayPeriodPrevDay = false;
442                    }
443    
444            }
445    
446        @Override
447        public List<ShiftDifferentialRule> getShiftDifferentialRules(String location, String hrSalGroup, String payGrade, Date fromEffdt, Date toEffdt, String active, String showHist) {
448            return shiftDifferentialRuleDao.getShiftDifferentialRules(location, hrSalGroup, payGrade, fromEffdt, toEffdt, active, showHist);
449        }
450    
451        private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) {
452            List<TimeBlock> filtered;
453    
454            if (blocks == null || blocks.size() == 0)
455                filtered = null;
456            else {
457                filtered = new ArrayList<TimeBlock>();
458                for (TimeBlock b : blocks) {
459                    if (timeBlockHasEarnCode(fromEarnGroup, b))
460                        filtered.add(b);
461                }
462            }
463    
464            return filtered;
465        }
466    
467        private void applyAccumulatedWrapper(BigDecimal accumHours,
468                                             Interval evalInterval,
469                                             List<Interval>accumulatedBlockIntervals,
470                                             List<TimeBlock>accumulatedBlocks,
471                                             List<TimeBlock> previousBlocks,
472                                             BigDecimal hoursToApplyPrevious,
473                                             BigDecimal hoursToApply,
474                                             ShiftDifferentialRule rule) {
475            if (accumHours.compareTo(rule.getMinHours()) >= 0) {
476                this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode());
477            }
478            accumulatedBlocks.clear();
479            accumulatedBlockIntervals.clear();
480        }
481    
482            private void sortTimeBlocksInverse(List<TimeBlock> blocks) {
483                    Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
484                            public int compare(TimeBlock tb1, TimeBlock tb2) {
485                                    if (tb1 != null && tb2 != null)
486                                            return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
487                                    return 0;
488                            }
489                    });
490            }
491    
492            private void sortTimeBlocksNatural(List<TimeBlock> blocks) {
493                    Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks
494                            public int compare(TimeBlock tb1, TimeBlock tb2) {
495                                    if (tb1 != null && tb2 != null)
496                                            return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp());
497                                    return 0;
498                            }
499                    });
500            }
501    
502        /**
503         *
504         * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block.
505         * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation.
506         * @param blocks The blocks we are applying hours to.
507         * @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.
508         * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL)
509         * @param hours hours to apply
510         * @param earnCode what earn code to create time hour detail entry for.
511         */
512            void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) {
513            for (int i=0; i<blocks.size(); i++) {
514                            TimeBlock b = blocks.get(i);
515    
516                // Only apply initial hours to the first timeblock.
517                            if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) {
518                    // ONLY if they're on the same document ID, do we apply to previous,
519                    // otherwise we dump all on the current document.
520                    if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) {
521                        for (TimeBlock pb : previousBlocks) {
522                            BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number
523                            initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours!
524                            if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply.
525                                break;
526    
527                            // Adjust hours on the block by the lunch sub hours, so we're not over applying.
528                            BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub));
529                            addPremiumTimeHourDetail(pb, hoursToApply, earnCode);
530                            initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
531                            if (initialHours.compareTo(BigDecimal.ZERO) <= 0)
532                                break;
533                        }
534                    } else {
535                                        addPremiumTimeHourDetail(b, initialHours, earnCode);
536                    }
537                }
538    
539                BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number
540                hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours!
541    
542                            if (hours.compareTo(BigDecimal.ZERO) > 0) {
543                    Interval blockInterval = blockIntervals.get(i);
544                    Interval overlapInterval = shift.overlap(blockInterval);
545                    if (overlapInterval == null)
546                        continue;
547    
548                    long overlap = overlapInterval.toDurationMillis();
549                    BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule
550                    // Adjust this time block's hoursMax (below) by lunchSub to
551                    // make sure the time applied is the correct amount per block.
552                    BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub));
553    
554                    addPremiumTimeHourDetail(b, hoursToApply, earnCode);
555                                    hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT);
556                            }
557                    }
558            }
559    
560            void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) {
561                    List<TimeHourDetail> details = block.getTimeHourDetails();
562                    TimeHourDetail premium = new TimeHourDetail();
563                    premium.setHours(hours);
564                    premium.setEarnCode(earnCode);
565                    premium.setTkTimeBlockId(block.getTkTimeBlockId());
566                    details.add(premium);
567            }
568    
569            /**
570             * Does the difference between the previous time blocks clock out time and the
571             * current time blocks clock in time exceed the max gap. max gap is in minutes
572             *
573             * @param previous
574             * @param current
575             * @param maxGap
576             * @return
577             */
578            boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) {
579                    long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime();
580                    BigDecimal gapMinutes = TKUtils.convertMillisToMinutes(difference);
581    
582                    return (gapMinutes.compareTo(maxGap) > 0);
583            }
584    
585            public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) {
586                    this.shiftDifferentialRuleDao = shiftDifferentialRuleDao;
587            }
588    
589            @Override
590            public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) {
591                    return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId);
592            }
593    
594            @Override
595            public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) {
596                    List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>();
597                    
598                    // location, sal group, pay grade, calendar
599                sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
600    
601                    // location, sal group, *, calendar
602                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate));
603    
604                    // location, *, pay grade, calendar
605                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate));
606    
607                    // location, *, *, calendar
608                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate));
609    
610                    // *, sal group, pay grade, calendar
611                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate));
612    
613                    // *, sal group, *, calendar
614                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate));
615    
616                    // *, *, pay grade, calendar
617                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate));
618    
619                    // *, *, *, calendar
620                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate));
621    
622                    // location, sal group, pay grade, *
623                sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, "%", asOfDate));
624    
625                    // location, sal group, *, *
626                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", "%", asOfDate));
627    
628                    // location, *, pay grade, *
629                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, "%", asOfDate));
630    
631                    // location, *, *, *
632                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", "%", asOfDate));
633    
634                    // *, sal group, pay grade, *
635                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, "%", asOfDate));
636    
637                    // *, sal group, *, *
638                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", "%", asOfDate));
639    
640                    // *, *, pay grade, *
641                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, "%", asOfDate));
642    
643                    // *, *, *, *
644                    sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", "%", asOfDate));
645    
646                    return sdrs;
647            }
648    
649            private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) {
650                    boolean active = false;
651    
652                    switch (currentDate.getDayOfWeek()) {
653                    case DateTimeConstants.MONDAY:
654                            active = sdr.isMonday();
655                            break;
656                    case DateTimeConstants.TUESDAY:
657                            active = sdr.isTuesday();
658                            break;
659                    case DateTimeConstants.WEDNESDAY:
660                            active = sdr.isWednesday();
661                            break;
662                    case DateTimeConstants.THURSDAY:
663                            active = sdr.isThursday();
664                            break;
665                    case DateTimeConstants.FRIDAY:
666                            active = sdr.isFriday();
667                            break;
668                    case DateTimeConstants.SATURDAY:
669                            active = sdr.isSaturday();
670                            break;
671                    case DateTimeConstants.SUNDAY:
672                            active = sdr.isSunday();
673                            break;
674                    }
675    
676                    return active;
677            }
678    
679            @Override
680            public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) {
681                    shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules);
682            }
683    
684            @Override
685            public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) {
686                    shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule);
687            }
688    
689    }