001 /** 002 * Copyright 2004-2012 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.hr.time.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.getPayCalendarEntry().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.getPayCalendarEntry().getBeginPeriodDateTime())); 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().getPayBeginDate()); 069 if (prevBlocks.size() > 0) { 070 TimesheetDocumentHeader prevTdh = TkServiceLocator.getTimesheetDocumentHeaderService().getPreviousDocumentHeader(timesheetDocument.getPrincipalId(), timesheetDocument.getDocumentHeader().getPayBeginDate()); 071 if (prevTdh != null) { 072 CalendarEntries prevPayCalendarEntry = TkServiceLocator.getCalendarService().getCalendarDatesByPayEndDate(timesheetDocument.getPrincipalId(), prevTdh.getPayEndDate(), null); 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.getPayCalendarEntry().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 (Long jobNumber: jobNumberToShifts.keySet()) { 195 List<ShiftDifferentialRule> shiftDifferentialRules = jobNumberToShifts.get(jobNumber); 196 // Obtain and sort our previous and current time blocks. 197 List<TimeBlock> ruleTimeBlocksPrev = null; 198 List<TimeBlock> ruleTimeBlocksCurr = jobNumberToTimeBlocks.get(jobNumber); 199 if (ruleTimeBlocksCurr != null && ruleTimeBlocksCurr.size() > 0) { 200 if (jobNumberToTimeBlocksPreviousDay != null) 201 ruleTimeBlocksPrev = jobNumberToTimeBlocksPreviousDay.get(jobNumber); 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.getEarnGroupService().getEarnCodeListForEarnGroup(rule.getFromEarnGroup(), TKUtils.getTimelessDate(timesheetDocument.getPayCalendarEntry().getBeginPeriodDateTime())); 214 215 // Because of the way java.sql.Time are stored, we need to first 216 // construct a LocalTime in the System Time Zone, then convert that 217 // time to the users time zone. 218 LocalTime ruleStart = new LocalTime(rule.getBeginTime(), TKUtils.getSystemDateTimeZone()); 219 LocalTime ruleEnd = new LocalTime(rule.getEndTime(), TKUtils.getSystemDateTimeZone()); 220 ruleStart = new LocalTime(ruleStart, zone); 221 ruleEnd = new LocalTime(ruleEnd, zone); 222 223 224 DateTime shiftEnd = ruleEnd.toDateTime(currentDay); 225 DateTime shiftStart = ruleStart.toDateTime(currentDay); 226 227 if (shiftEnd.isBefore(shiftStart) || shiftEnd.isEqual(shiftStart)) 228 shiftEnd = shiftEnd.plusDays(1); 229 Interval shiftInterval = new Interval(shiftStart, shiftEnd); 230 231 // Set up buckets to handle previous days time accumulations 232 BigDecimal hoursBeforeVirtualDay = BigDecimal.ZERO; 233 234 // Check current day first block to see if start time gap from virtual day start is greater than max gap 235 // if so, we can skip the previous day checks. 236 TimeBlock firstBlockOfCurrentDay = null; 237 for (TimeBlock b : ruleTimeBlocksCurr) { 238 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 239 firstBlockOfCurrentDay = b; 240 break; 241 } 242 } 243 244 // Previous Day :: We have prior block container of nonzero size, and the previous day is active. 245 Interval previousDayShiftInterval = new Interval(shiftStart.minusDays(1), shiftEnd.minusDays(1)); 246 247 // Blank initialization pointer for picking which interval to pass to applyPremium() 248 Interval evalInterval = null; 249 if (ruleTimeBlocksPrev != null && ruleTimeBlocksPrev.size() > 0 && dayIsRuleActive(currentDay.minusDays(1), rule)) { 250 // Simple heuristic to see if we even need to worry about 251 // the Shift rule for this set of data. 252 if (shiftEnd.isAfter(virtualDay.getEnd())) { 253 // Compare first block of previous day with first block of current day for max gaptitude. 254 TimeBlock firstBlockOfPreviousDay = null; 255 for (TimeBlock b : ruleTimeBlocksPrev) { 256 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 257 firstBlockOfPreviousDay = b; 258 break; 259 } 260 } 261 // Only if we actually have at least one block. 262 // Adding Assumption: We must have both a valid current and previous block. Max Gap can not be more than a virtual day. 263 // If this assumption does not hold, additional logic will be needed to iteratively go back in time to figure out which 264 // blocks are valid. 265 if ( (firstBlockOfPreviousDay != null) && (firstBlockOfCurrentDay != null)) { 266 Interval previousBlockInterval = new Interval(new DateTime(firstBlockOfPreviousDay.getEndTimestamp(), zone), new DateTime(firstBlockOfCurrentDay.getBeginTimestamp(), zone)); 267 Duration blockGapDuration = previousBlockInterval.toDuration(); 268 BigDecimal bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); 269 if (bgdHours.compareTo(rule.getMaxGap()) <= 0) { 270 // If we are here, we know we have at least one valid time block to pull some hours forward from. 271 272 273 // These are inversely sorted. 274 for (int i=0; i<ruleTimeBlocksPrev.size(); i++) { 275 TimeBlock b = ruleTimeBlocksPrev.get(i); 276 if (timeBlockHasEarnCode(fromEarnGroup, b)) { 277 Interval blockInterval = new Interval(new DateTime(b.getBeginTimestamp(), zone), new DateTime(b.getEndTimestamp(), zone)); 278 279 // Calculate Block Gap, the duration between clock outs and clock ins of adjacent time blocks. 280 if (previousBlockInterval != null) { 281 blockGapDuration = new Duration(new DateTime(b.getEndTimestamp(), zone), previousBlockInterval.getStart()); 282 bgdHours = TKUtils.convertMillisToHours(blockGapDuration.getMillis()); 283 } 284 285 // Check Gap, if good, sum hours 286 if (bgdHours.compareTo(rule.getMaxGap()) <= 0) { 287 // Calculate Overlap and add it to hours before virtual day bucket. 288 if (blockInterval.overlaps(previousDayShiftInterval)) { 289 BigDecimal hrs = TKUtils.convertMillisToHours(blockInterval.overlap(previousDayShiftInterval).toDurationMillis()); 290 hoursBeforeVirtualDay = hoursBeforeVirtualDay.add(hrs); 291 } 292 293 } else { 294 // Time blocks are reverse sorted, we can jump out as soon as the max gap is exceeded. 295 break; 296 } 297 298 previousBlockInterval = blockInterval; 299 300 } 301 } 302 } else { 303 // DO NOTHING! 304 } 305 } 306 } 307 } 308 309 BigDecimal hoursToApply = BigDecimal.ZERO; 310 BigDecimal hoursToApplyPrevious = BigDecimal.ZERO; 311 // If the hours before virtual day are less than or equal to 312 // min hours, we have already applied the time, so we don't 313 // set hoursToApplyPrevious 314 if (hoursBeforeVirtualDay.compareTo(rule.getMinHours()) <= 0) { 315 // we need to apply these hours. 316 hoursToApplyPrevious = hoursBeforeVirtualDay; 317 } 318 319 320 // Current Day 321 322 TimeBlock previous = null; // Previous Time Block 323 List<TimeBlock> accumulatedBlocks = new ArrayList<TimeBlock>(); // TimeBlocks we MAY or MAY NOT apply Shift Premium to. 324 List<Interval> accumulatedBlockIntervals = new ArrayList<Interval>(); // To save recompute time when checking timeblocks for application we store them as we create them. 325 // Iterate over sorted list, checking time boundaries vs Shift Intervals. 326 long accumulatedMillis = TKUtils.convertHoursToMillis(hoursBeforeVirtualDay); 327 328 boolean previousDayOnly = false; // IF the rule is not active today, but was on the previous day, we need to still look at time blocks. 329 if (!dayIsRuleActive(currentDay, rule)) { 330 if (dayIsRuleActive(currentDay.minusDays(1), rule)) { 331 previousDayOnly = true; 332 } else { 333 // Nothing to see here, move to next rule. 334 continue; 335 } 336 337 } 338 339 /* 340 * We will touch each time block and accumulate time blocks that are applicable to 341 * the current rule we are on. 342 */ 343 344 // These blocks are only used for detail application 345 // We don't want to pass along the previous pay period, 346 // because we don't want to modify the time blocks on that 347 // period. If null is passed, time will be placed on the 348 // first block of the first period if the previous period 349 // block had influence. 350 List<TimeBlock> previousBlocksFiltered = (previousPayPeriodPrevDay) ? null : filterBlocksByApplicableEarnGroup(fromEarnGroup, ruleTimeBlocksPrev); 351 352 for (TimeBlock current : ruleTimeBlocksCurr) { 353 if (!timeBlockHasEarnCode(fromEarnGroup, current)) { 354 // TODO: WorkSchedule considerations somewhere in here? 355 continue; 356 } 357 358 Interval blockInterval = new Interval(new DateTime(current.getBeginTimestamp(), zone), new DateTime(current.getEndTimestamp(), zone)); 359 360 // Check both Intervals, since the time blocks could still 361 // be applicable to the previous day. These two intervals should 362 // not have any overlap. 363 if (previousDayShiftInterval.overlaps(shiftInterval)) 364 throw new RuntimeException("Interval of greater than 24 hours created in the rules processing."); 365 366 // This block of code handles cases where you have time 367 // that spills to multiple days and a shift rule that 368 // has a valid window on multiple consecutive days. Time 369 // must be applied with the correct shift interval. 370 Interval overlap = previousDayShiftInterval.overlap(blockInterval); 371 evalInterval = previousDayShiftInterval; 372 if (overlap == null) { 373 if (hoursToApplyPrevious.compareTo(BigDecimal.ZERO) > 0) { 374 // we have hours from previous day, and the shift 375 // window is going to move to current day. 376 // Need to apply this now, and move window forward 377 // for current time block. 378 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 379 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 380 accumulatedMillis = 0L; // reset accumulated hours.. 381 hoursToApply = BigDecimal.ZERO; 382 hoursToApplyPrevious = BigDecimal.ZERO; 383 } 384 385 // Because of our position in the loop, when we are at this point, 386 // we know we've passed any previous day shift intervals, so we can 387 // determine if we should skip the current day based on the boolean 388 // we set earlier. 389 if (previousDayOnly) { 390 continue; 391 } 392 393 overlap = shiftInterval.overlap(blockInterval); 394 evalInterval = shiftInterval; 395 } 396 397 // Time bucketing and application as normal: 398 // 399 if (overlap != null) { 400 // There IS overlap. 401 if (previous != null) { 402 if (exceedsMaxGap(previous, current, rule.getMaxGap())) { 403 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 404 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 405 accumulatedMillis = 0L; // reset accumulated hours.. 406 hoursToApply = BigDecimal.ZERO; 407 hoursToApplyPrevious = BigDecimal.ZERO; 408 } else { 409 long millis = overlap.toDurationMillis(); 410 accumulatedMillis += millis; 411 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); 412 } 413 } else { 414 // Overlap shift at first time block. 415 long millis = overlap.toDurationMillis(); 416 accumulatedMillis += millis; 417 hoursToApply = hoursToApply.add(TKUtils.convertMillisToHours(millis)); 418 } 419 accumulatedBlocks.add(current); 420 accumulatedBlockIntervals.add(blockInterval); 421 previous = current; // current can still apply to next. 422 } else { 423 // No Overlap / Outside of Rule 424 if (previous != null) { 425 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 426 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 427 accumulatedMillis = 0L; // reset accumulated hours.. 428 hoursToApply = BigDecimal.ZERO; 429 hoursToApplyPrevious = BigDecimal.ZERO; 430 } 431 } 432 433 } 434 435 // All time blocks are iterated over, check for remainders. 436 // Check containers for time, and apply if needed. 437 BigDecimal accumHours = TKUtils.convertMillisToHours(accumulatedMillis); 438 this.applyAccumulatedWrapper(accumHours, evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocksFiltered, hoursToApplyPrevious, hoursToApply, rule); 439 } 440 } 441 // Keep track of previous as we move day by day. 442 jobNumberToTimeBlocksPreviousDay = jobNumberToTimeBlocks; 443 previousPayPeriodPrevDay = false; 444 } 445 446 } 447 448 private List<TimeBlock> filterBlocksByApplicableEarnGroup(Set<String> fromEarnGroup, List<TimeBlock> blocks) { 449 List<TimeBlock> filtered; 450 451 if (blocks == null || blocks.size() == 0) 452 filtered = null; 453 else { 454 filtered = new ArrayList<TimeBlock>(); 455 for (TimeBlock b : blocks) { 456 if (timeBlockHasEarnCode(fromEarnGroup, b)) 457 filtered.add(b); 458 } 459 } 460 461 return filtered; 462 } 463 464 465 private void applyAccumulatedWrapper(BigDecimal accumHours, Interval evalInterval, List<Interval>accumulatedBlockIntervals, List<TimeBlock>accumulatedBlocks, List<TimeBlock> previousBlocks, BigDecimal hoursToApplyPrevious, BigDecimal hoursToApply, ShiftDifferentialRule rule) { 466 if (accumHours.compareTo(rule.getMinHours()) >= 0) { 467 this.applyPremium(evalInterval, accumulatedBlockIntervals, accumulatedBlocks, previousBlocks, hoursToApplyPrevious, hoursToApply, rule.getEarnCode()); 468 } 469 accumulatedBlocks.clear(); 470 accumulatedBlockIntervals.clear(); 471 } 472 473 private void sortTimeBlocksInverse(List<TimeBlock> blocks) { 474 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 475 public int compare(TimeBlock tb1, TimeBlock tb2) { 476 if (tb1 != null && tb2 != null) 477 return -1 * tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 478 return 0; 479 } 480 }); 481 } 482 483 private void sortTimeBlocksNatural(List<TimeBlock> blocks) { 484 Collections.sort(blocks, new Comparator<TimeBlock>() { // Sort the Time Blocks 485 public int compare(TimeBlock tb1, TimeBlock tb2) { 486 if (tb1 != null && tb2 != null) 487 return tb1.getBeginTimestamp().compareTo(tb2.getBeginTimestamp()); 488 return 0; 489 } 490 }); 491 } 492 493 /** 494 * 495 * @param shift The shift interval - need to examine the time block to determine how many hours are eligible per block. 496 * @param blockIntervals Intervals for each block present in the blocks list. Passed here to avoid re computation. 497 * @param blocks The blocks we are applying hours to. 498 * @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. 499 * @param initialHours hours accumulated from a previous boundary that need to be applied here (NOT SUBJECT TO INTERVAL) 500 * @param hours hours to apply 501 * @param earnCode what earn code to create time hour detail entry for. 502 */ 503 void applyPremium(Interval shift, List<Interval> blockIntervals, List<TimeBlock> blocks, List<TimeBlock> previousBlocks, BigDecimal initialHours, BigDecimal hours, String earnCode) { 504 for (int i=0; i<blocks.size(); i++) { 505 TimeBlock b = blocks.get(i); 506 507 // Only apply initial hours to the first timeblock. 508 if (i == 0 && (initialHours.compareTo(BigDecimal.ZERO) > 0)) { 509 // ONLY if they're on the same document ID, do we apply to previous, 510 // otherwise we dump all on the current document. 511 if (previousBlocks != null && previousBlocks.size() > 0 && previousBlocks.get(0).getDocumentId().equals(b.getDocumentId())) { 512 for (TimeBlock pb : previousBlocks) { 513 BigDecimal lunchSub = this.negativeTimeHourDetailSum(pb); // A negative number 514 initialHours = BigDecimal.ZERO.max(initialHours.add(lunchSub)); // We don't want negative premium hours! 515 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) // check here now as well, we may not have anything at all to apply. 516 break; 517 518 // Adjust hours on the block by the lunch sub hours, so we're not over applying. 519 BigDecimal hoursToApply = initialHours.min(pb.getHours().add(lunchSub)); 520 addPremiumTimeHourDetail(pb, hoursToApply, earnCode); 521 initialHours = initialHours.subtract(hoursToApply, TkConstants.MATH_CONTEXT); 522 if (initialHours.compareTo(BigDecimal.ZERO) <= 0) 523 break; 524 } 525 } else { 526 addPremiumTimeHourDetail(b, initialHours, earnCode); 527 } 528 } 529 530 BigDecimal lunchSub = this.negativeTimeHourDetailSum(b); // A negative number 531 hours = BigDecimal.ZERO.max(hours.add(lunchSub)); // We don't want negative premium hours! 532 533 if (hours.compareTo(BigDecimal.ZERO) > 0) { 534 Interval blockInterval = blockIntervals.get(i); 535 Interval overlapInterval = shift.overlap(blockInterval); 536 if (overlapInterval == null) 537 continue; 538 539 long overlap = overlapInterval.toDurationMillis(); 540 BigDecimal hoursMax = TKUtils.convertMillisToHours(overlap); // Maximum number of possible hours applicable for this time block and shift rule 541 // Adjust this time block's hoursMax (below) by lunchSub to 542 // make sure the time applied is the correct amount per block. 543 BigDecimal hoursToApply = hours.min(hoursMax.add(lunchSub)); 544 545 addPremiumTimeHourDetail(b, hoursToApply, earnCode); 546 hours = hours.subtract(hoursToApply, TkConstants.MATH_CONTEXT); 547 } 548 } 549 } 550 551 void addPremiumTimeHourDetail(TimeBlock block, BigDecimal hours, String earnCode) { 552 List<TimeHourDetail> details = block.getTimeHourDetails(); 553 TimeHourDetail premium = new TimeHourDetail(); 554 premium.setHours(hours); 555 premium.setEarnCode(earnCode); 556 premium.setTkTimeBlockId(block.getTkTimeBlockId()); 557 details.add(premium); 558 } 559 560 /** 561 * Does the difference between the previous time blocks clock out time and the 562 * current time blocks clock in time exceed the max gap? 563 * 564 * @param previous 565 * @param current 566 * @param maxGap 567 * @return 568 */ 569 boolean exceedsMaxGap(TimeBlock previous, TimeBlock current, BigDecimal maxGap) { 570 long difference = current.getBeginTimestamp().getTime() - previous.getEndTimestamp().getTime(); 571 BigDecimal gapHours = TKUtils.convertMillisToHours(difference); 572 573 return (gapHours.compareTo(maxGap) > 0); 574 } 575 576 public void setShiftDifferentialRuleDao(ShiftDifferentialRuleDao shiftDifferentialRuleDao) { 577 this.shiftDifferentialRuleDao = shiftDifferentialRuleDao; 578 } 579 580 @Override 581 public ShiftDifferentialRule getShiftDifferentialRule(String tkShiftDifferentialRuleId) { 582 return this.shiftDifferentialRuleDao.findShiftDifferentialRule(tkShiftDifferentialRuleId); 583 } 584 585 @Override 586 public List<ShiftDifferentialRule> getShiftDifferentalRules(String location, String hrSalGroup, String payGrade, String pyCalendarGroup, Date asOfDate) { 587 List<ShiftDifferentialRule> sdrs = new ArrayList<ShiftDifferentialRule>(); 588 589 // location, sal group, pay grade 590 591 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); 592 593 // location, sal group, * 594 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, hrSalGroup, "%", pyCalendarGroup, asOfDate)); 595 596 // location, *, pay grade 597 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", payGrade, pyCalendarGroup, asOfDate)); 598 599 // location, *, * 600 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules(location, "%", "%", pyCalendarGroup, asOfDate)); 601 602 // *, sal group, pay grade 603 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, payGrade, pyCalendarGroup, asOfDate)); 604 605 // *, sal group, * 606 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", hrSalGroup, "%", pyCalendarGroup, asOfDate)); 607 608 // *, *, pay grade 609 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", payGrade, pyCalendarGroup, asOfDate)); 610 611 // *, *, * 612 sdrs.addAll(shiftDifferentialRuleDao.findShiftDifferentialRules("%", "%", "%", pyCalendarGroup, asOfDate)); 613 614 if (sdrs == null) 615 sdrs = Collections.emptyList(); 616 617 return sdrs; 618 } 619 620 private boolean dayIsRuleActive(DateTime currentDate, ShiftDifferentialRule sdr) { 621 boolean active = false; 622 623 switch (currentDate.getDayOfWeek()) { 624 case DateTimeConstants.MONDAY: 625 active = sdr.isMonday(); 626 break; 627 case DateTimeConstants.TUESDAY: 628 active = sdr.isTuesday(); 629 break; 630 case DateTimeConstants.WEDNESDAY: 631 active = sdr.isWednesday(); 632 break; 633 case DateTimeConstants.THURSDAY: 634 active = sdr.isThursday(); 635 break; 636 case DateTimeConstants.FRIDAY: 637 active = sdr.isFriday(); 638 break; 639 case DateTimeConstants.SATURDAY: 640 active = sdr.isSaturday(); 641 break; 642 case DateTimeConstants.SUNDAY: 643 active = sdr.isSunday(); 644 break; 645 } 646 647 return active; 648 } 649 650 @Override 651 public void saveOrUpdate(List<ShiftDifferentialRule> shiftDifferentialRules) { 652 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRules); 653 } 654 655 @Override 656 public void saveOrUpdate(ShiftDifferentialRule shiftDifferentialRule) { 657 shiftDifferentialRuleDao.saveOrUpdate(shiftDifferentialRule); 658 } 659 660 }