001/**
002 * Copyright 2004-2014 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 */
016package org.kuali.hr.time.shiftdiff.rule;
017
018import com.google.common.collect.ImmutableMap;
019import org.joda.time.DateTime;
020import org.joda.time.DateTimeZone;
021import org.joda.time.LocalTime;
022import org.junit.Assert;
023import org.junit.Ignore;
024import org.junit.Test;
025import org.kuali.hr.KPMEWebTestCase;
026import org.kuali.kpme.core.FunctionalTest;
027import org.kuali.kpme.core.api.assignment.Assignment;
028import org.kuali.kpme.core.api.assignment.AssignmentDescriptionKey;
029import org.kuali.kpme.core.api.calendar.entry.CalendarEntry;
030import org.kuali.kpme.core.calendar.CalendarBo;
031import org.kuali.kpme.core.service.HrServiceLocator;
032import org.kuali.kpme.core.util.TKUtils;
033import org.kuali.kpme.tklm.api.time.timeblock.TimeBlock;
034import org.kuali.kpme.tklm.time.rules.shiftdifferential.ShiftDifferentialRule;
035import org.kuali.kpme.tklm.time.rules.shiftdifferential.service.ShiftDifferentialRuleService;
036import org.kuali.kpme.tklm.time.service.TkServiceLocator;
037import org.kuali.kpme.tklm.time.timesheet.TimesheetDocument;
038import org.kuali.kpme.tklm.time.util.TkTimeBlockAggregate;
039import org.kuali.kpme.tklm.utils.TkTestUtils;
040
041import java.math.BigDecimal;
042import java.sql.Time;
043import java.util.ArrayList;
044import java.util.HashMap;
045import java.util.List;
046
047/**
048 *
049 * @author djunk
050 *
051 */
052@FunctionalTest
053public class ShiftDifferentialRuleServiceProcessTest extends KPMEWebTestCase {
054
055
056        public static final String USER_PRINCIPAL_ID = "admin";
057        private DateTime JAN_AS_OF_DATE = new DateTime(2010, 1, 1, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
058
059
060        /**
061         * Test with boundary carryover and overlapping rules.
062         *
063         * Rule 1:
064         *
065         * Runs on Tu, Wed, Th on the interval: [22:00, 4:00)
066         * Max Gap: 15 minutes
067         * Min Hours: 3
068         *
069         * Rule 2:
070         *
071         * Runs on Tu, Th on the interval: [23:00, 2:00)
072         * Max Gap: 2 hours
073         * Min Hours: 3
074         *
075         * Rule 3:
076         *
077         * Runs on W, Th on the interval: [5:00, 12:00)
078         * Max Gap: 15 minutes
079         * Min Hours: 7 hours
080         *
081         * Rule 4:
082         *
083         * Runs on W on the interval: [5:00, 12:00)
084         * Max Gap: 15 minutes
085         * Min Hours: 5
086         *
087         *
088         * |--------------+----+------------+------------|
089         * | Tu : 8/31/10 | XX | W : 9/1/10 | Th: 9/2/10 |
090         * |--------------+----+------------+------------|
091         * | 9:45p - 11:45| XX | Mid - 5a   | 5p - 11p   |
092         * |              | XX | 6a - Noon  |            |
093         * |--------------+----+------------+------------|
094     *
095     *
096     * Aug 31: 2h  : 21:45 - 23:45 (Tue) **
097     *                           [1: 5h 45m]  // [2: 2h 45m] - Not qualifying, min hours must be 3.
098     * Sep  1: 5h  : 00:00 - 05:00 (Wed) **
099     * Sep  1: 6h  : 06:00 - 12:00 (Wed) [4: 6h]
100     *
101     * Sep  1: 2h  : 22:00 - 24:00 (Wed)
102     * Sep  2: 1h  : 00:00 - 01:00 (Thu) [1: 3h]
103     *
104     * Sep  2: 6h  : 17:00 - 22:00 (Thu)
105     *
106     * 1: [22:00,  4:00) (Tue/Wed/Thu) minimum: 3h gap: 15m
107     * 2: [23:00,  2:00) (Tue/Thu)     minimum: 3h gap: 2h
108     * 3: [05:00, 12:00) (Wed/Thu)     minimum: 7h gap: 15m
109     * 4: [05:00, 12:00) (Wed)         minimum: 5h gap: 15m
110     *
111         */
112        @SuppressWarnings("serial")
113        @Test
114        public void testProcessTimesheetBoundaryCarryoverOverlapCase() throws Exception {
115        DateTimeZone tz = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
116                // Create the Rule    Sun,   Mon,   Tue,  Wed,   Thu,  Fri,  Sat
117                boolean[] dayArray = {false, false, true, true, true, true, true};
118                // Matches HR Job ID #1 (job # 30)
119                Long jobNumber = 30L;
120                Long workArea = 0L;
121                this.createShiftDifferentialRule(
122                                "BWS-CAL", "REG", "PRM", "IN", "SD1", "SD1",// // changed from "SD1" to "IN" for changes of adding groupKeyCode to Job
123                                (new LocalTime(22, 0)),
124                                (new LocalTime(4, 0)),
125                                new BigDecimal(3), // minHours
126                                new BigDecimal("15.00"), // maxGap
127                                dayArray);
128
129
130                // Timeblocks
131
132                // August
133                DateTime beginPeriodDate = new DateTime(2010, 8, 15, 0, 0, 0, 0, tz);
134                CalendarEntry endOfAugust =  HrServiceLocator.getCalendarEntryService().getCalendarEntryByIdAndPeriodEndDate("2", new DateTime(2010, 9, 1, 0, 0, 0, 0));
135                DateTime start = new DateTime(2010, 8, 31, 21, 45, 0, 0, tz);
136                List<TimeBlock> blocks = new ArrayList<TimeBlock>();
137                TimesheetDocument tdoc = TkServiceLocator.getTimesheetService().openTimesheetDocument("admin", endOfAugust);
138        Assignment assignment = HrServiceLocator.getAssignmentService().getAssignment("admin", AssignmentDescriptionKey.get("IU-IN_30_30_30"), beginPeriodDate.toLocalDate());
139                blocks.addAll(TkTestUtils.createUniformActualTimeBlocks(tdoc, assignment, "RGN", start, 1, new BigDecimal(2), BigDecimal.ZERO, "admin"));
140                TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, endOfAugust, HrServiceLocator.getCalendarService().getCalendar(endOfAugust.getHrCalendarId()), true);
141                tdoc.setTimeBlocks(blocks);
142                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
143                TkTestUtils.verifyAggregateHourSumsFlatList("August Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("RGN", new BigDecimal(2));}},aggregate);
144                TkServiceLocator.getTimeBlockService().saveOrUpdateTimeBlocks(new ArrayList<TimeBlock>(), aggregate.getFlattenedTimeBlockList(), "admin");
145
146
147                // September
148
149                start = new DateTime(2010, 9, 1, 0, 0, 0, 0, tz);
150                CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start.toLocalDate().toDateTimeAtStartOfDay());
151                tdoc = TkServiceLocator.getTimesheetService().openTimesheetDocument("admin", payCalendarEntry);
152                blocks = new ArrayList<TimeBlock>();
153                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start, 1, new BigDecimal("5"), "RGN", jobNumber, workArea));
154                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start.plusHours(6), 1, new BigDecimal("6"), "RGN", jobNumber, workArea));
155        blocks.addAll(TkTestUtils.createUniformTimeBlocks(start.plusHours(22), 1, new BigDecimal("2"), "RGN", jobNumber, workArea));
156        blocks.addAll(TkTestUtils.createUniformTimeBlocks(start.plusDays(1), 1, new BigDecimal("1"), "RGN", jobNumber, workArea));
157                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start.plusDays(1).plusHours(17), 1, new BigDecimal("6"), "RGN", jobNumber, workArea));
158                blocks = setDocumentIdOnBlocks(blocks, tdoc.getDocumentId());
159        
160                aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry, HrServiceLocator.getCalendarService().getCalendar(payCalendarEntry.getHrCalendarId()), true);
161                
162                TkTestUtils.verifyAggregateHourSumsFlatList("September Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("RGN", new BigDecimal(20));}},aggregate);
163
164                // Verify carry over and applied PRM bucket
165                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
166                TkTestUtils.verifyAggregateHourSumsFlatList("September Post-Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal("8.75"));put("RGN", new BigDecimal(22));}},aggregate);
167        }
168
169    private List<TimeBlock> setDocumentIdOnBlocks(List<TimeBlock> blocks, String id) {
170        List<TimeBlock> updatedTimeBlocks = new ArrayList<TimeBlock>();
171        for (TimeBlock b : blocks) {
172            TimeBlock.Builder builder = TimeBlock.Builder.create(b);
173            builder.setDocumentId(id);
174            updatedTimeBlocks.add(builder.build());
175        }
176        return updatedTimeBlocks;
177    }
178
179
180        /**
181         * Test where previous time sheet contains hours that should be added to
182         * the next pay periods first day shift.
183         *
184         * Runs on Tu, Th on the interval: [22:00, 4:00)
185         * Max Gap: 15 minutes
186         * Min Hours: 3
187         *
188         * |--------------+----+------------+-------------|
189         * | Tu : 8/31/10 | XX | W : 9/1/10 | Th : 9/2/10 |
190         * |--------------+----+------------+-------------|
191         * | 10pm - Mid   | XX | Mid - 5am  | 5pm - 11pm  |
192         * |--------------+----+------------+-------------|
193         *
194         * @throws Exception
195         */
196        @SuppressWarnings("serial")
197        @Test
198        public void testProcessShiftTimesheeetBoundaryCarryoverCase() throws Exception {
199                // Create the Rule    Sun,   Mon,   Tue,  Wed,   Thu,  Fri,  Sat
200                boolean[] dayArray = {false, false, true, false, true, true, true};
201                // Matches HR Job ID #1 (job # 30)
202                Long jobNumber = 30L;
203                Long workArea = 0L;
204
205        DateTimeZone tz = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
206                this.createShiftDifferentialRule(
207                                "BWS-CAL",
208                                "REG",
209                                "PRM",
210                                "IN",  // changed from "SD1" to "IN" for changes of adding groupKeyCode to Job
211                                "SD1",
212                                "SD1",
213                                (new LocalTime(22, 0)),
214                                (new LocalTime(5, 0)),
215                                new BigDecimal(3), // minHours
216                                new BigDecimal("0.25"), // maxGap
217                                dayArray);
218
219                // August
220                DateTime endPeriodDate = new DateTime(2010, 9, 1, 0, 0, 0, 0);
221        CalendarEntry endOfAugust =  HrServiceLocator.getCalendarEntryService().getCalendarEntryByIdAndPeriodEndDate("2", endPeriodDate);
222                DateTime start = new DateTime(2010, 8, 31, 22, 0, 0, 0, tz);
223                List<TimeBlock> blocks = new ArrayList<TimeBlock>();
224                TimesheetDocument tdoc = TkServiceLocator.getTimesheetService().openTimesheetDocument("admin", endOfAugust);
225        Assignment assignment = HrServiceLocator.getAssignmentService().getAssignment("admin", AssignmentDescriptionKey.get("IU-IN_30_30_30"), endOfAugust.getBeginPeriodFullDateTime().toLocalDate());
226                blocks.addAll(TkTestUtils.createUniformActualTimeBlocks(tdoc, assignment, "RGN", start, 1, new BigDecimal(2), BigDecimal.ZERO, "admin"));
227                TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, endOfAugust, HrServiceLocator.getCalendarService().getCalendar(endOfAugust.getHrCalendarId()), true);
228
229
230
231                tdoc.setTimeBlocks(blocks);
232                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
233                TkTestUtils.verifyAggregateHourSumsFlatList("August Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("RGN", new BigDecimal(2));}},aggregate);
234                TkServiceLocator.getTimeBlockService().saveOrUpdateTimeBlocks(new ArrayList<TimeBlock>(), aggregate.getFlattenedTimeBlockList(), "admin");
235
236
237                // September
238                start = new DateTime(2010, 9, 1, 0, 0, 0, 0, tz);
239                CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start.toLocalDate().toDateTimeAtStartOfDay());
240                tdoc = TkServiceLocator.getTimesheetService().openTimesheetDocument("admin", payCalendarEntry);
241                blocks = new ArrayList<TimeBlock>();
242                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start, 1, new BigDecimal("5"), "RGN", jobNumber, workArea));
243                aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry, HrServiceLocator.getCalendarService().getCalendar(payCalendarEntry.getHrCalendarId()), true);
244                TkTestUtils.verifyAggregateHourSumsFlatList("September Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("RGN", new BigDecimal(5));}},aggregate);
245
246                // Verify carry over and applied PRM bucket
247                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
248                TkTestUtils.verifyAggregateHourSumsFlatList("September Post-Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal(7));put("RGN", new BigDecimal(7));}},aggregate);
249        }
250
251        @SuppressWarnings("serial")
252        @Test
253        /**
254         * Runs on every day on the interval: [16:00, 24:00)
255         * Max Gap: 15 minutes
256         * Min Hours: 4
257         *
258         * Added some extra time blocks that are not in the shift interval, but
259         * close to the time blocks that are.
260         *
261         * @throws Exception
262         */
263        public void testProcessShiftSimpleNoisyCase() throws Exception {
264                // Create the Rule
265                boolean[] dayArray = {true, true, true, true, true, true, true};
266                // Matches HR Job ID #1 (job # 30)
267                Long jobNumber = 30L;
268                Long workArea = 0L;
269        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
270                this.createShiftDifferentialRule(
271                                "BWS-CAL",
272                                "REG",
273                                "PRM",
274                                "IN", // changed from "SD1" to "IN" for changes of adding groupKeyCode to Job
275                                "SD1",
276                                "SD1",
277                                (new LocalTime(16, 0)),
278                                (new LocalTime(0, 0)),
279                                new BigDecimal(4), // minHours
280                                new BigDecimal("15"), // maxGap
281                                dayArray);
282
283                // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
284                DateTime start = new DateTime(2010, 3, 29, 14, 0, 0, 0, zone);
285                List<TimeBlock> blocks = new ArrayList<TimeBlock>();
286                CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
287                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start, 2, new BigDecimal("4"), "RGN", jobNumber, workArea));
288                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start.plusHours(4).plusMinutes(15), 2, new BigDecimal("2"), "RGN", jobNumber, workArea));
289                blocks.addAll(TkTestUtils.createUniformTimeBlocks(new DateTime(2010, 3, 29, 12, 58, 0, 0, zone), 2, new BigDecimal(1), "RGN", jobNumber, workArea));
290                TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
291
292                // Verify pre-Rule Run
293                TkTestUtils.verifyAggregateHourSums("Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("RGN", new BigDecimal(14));}},aggregate,2);
294
295                // Run Rule
296                TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
297                tdoc.setTimeBlocks(blocks);
298                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
299
300                // Verify post-Rule Run
301                TkTestUtils.verifyAggregateHourSums("Post Rules Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal(8));put("RGN", new BigDecimal(14));}},aggregate,2);
302        }
303
304        @SuppressWarnings("serial")
305        @Test
306        /**
307         * Runs on every day on the interval: [16:00, 24:00)
308         * Max Gap: 15 minutes
309         * Min Hours: 4
310         *
311         * @throws Exception
312         */
313        public void testProcessShiftSimpleCase() throws Exception {
314                // Create the Rule
315                boolean[] dayArray = {true, true, true, true, true, true, true};
316                // Matches HR Job ID #1 (job # 30)
317                Long jobNumber = 30L;
318                Long workArea = 0L;
319        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
320                this.createShiftDifferentialRule(
321                                "BWS-CAL",
322                                "REG",
323                                "PRM",
324                                "IN", // changed from "SD1" to "IN" for changes of adding groupKeyCode to Job
325                                "SD1",
326                                "SD1",
327                                (new LocalTime(16, 0)), //4pm
328                                (new LocalTime(0, 0)),  //midnight
329                                new BigDecimal(4), // minHours
330                                new BigDecimal("15.00"), // maxGap minutes
331                                dayArray);
332
333                // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
334                DateTime start = new DateTime(2010, 3, 29, 14, 0, 0, 0, zone);
335                List<TimeBlock> blocks = new ArrayList<TimeBlock>();
336                CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
337                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start, 2, new BigDecimal("4"), "REG", jobNumber, workArea));
338                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start.plusHours(4).plusMinutes(15), 2, new BigDecimal("2"), "REG", jobNumber, workArea));
339                TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
340
341                // Verify pre-Rule Run
342                TkTestUtils.verifyAggregateHourSums("Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("REG", new BigDecimal(12));}},aggregate,2);
343
344                // Run Rule
345                TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
346                tdoc.setTimeBlocks(blocks);
347                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
348
349                // Verify post-Rule Run
350                TkTestUtils.verifyAggregateHourSums("Post Rules Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal(8));put("REG", new BigDecimal(12));}},aggregate,2);
351        }
352
353        /**
354         * Stores the Shift Differential Rule in the database for testing.
355         *
356         * dayBooleans[] is a 7 element array of booleans, [0, 6] is [sun, sat]
357         */
358        private void createShiftDifferentialRule(String pyCalendarGroup, String fromEarnGroup, String premiumEarnCode, String location, String payGrade, String hrSalGroup, LocalTime startTime, LocalTime endTime, BigDecimal minHours, BigDecimal maxGap, boolean dayBooleans[]) {
359                Assert.assertTrue("Wrong number of day booleans", dayBooleans.length == 7);
360
361                ShiftDifferentialRuleService service = TkServiceLocator.getShiftDifferentialRuleService();
362                ShiftDifferentialRule sdr = new ShiftDifferentialRule();
363
364                sdr.setBeginTime(new Time(startTime.toDateTimeToday().getMillis()));
365                sdr.setEndTime(new Time(endTime.toDateTimeToday().getMillis()));
366                sdr.setMinHours(minHours);
367                sdr.setMaxGap(maxGap);
368                sdr.setActive(true);
369                sdr.setUserPrincipalId(USER_PRINCIPAL_ID);
370                sdr.setEffectiveLocalDate(JAN_AS_OF_DATE.toLocalDate());
371                sdr.setLocation(location);
372                sdr.setPayGrade(payGrade);
373                sdr.setHrSalGroup(hrSalGroup);
374                sdr.setFromEarnGroup(fromEarnGroup);
375                sdr.setPyCalendarGroup(pyCalendarGroup);
376                sdr.setEarnCode(premiumEarnCode);
377        sdr.setRuleType("default");
378
379                for (int i=0; i<dayBooleans.length; i++) {
380                        switch(i) {
381                        case 0:
382                                sdr.setSunday(dayBooleans[i]);
383                                break;
384                        case 1:
385                                sdr.setMonday(dayBooleans[i]);
386                                break;
387                        case 2:
388                                sdr.setTuesday(dayBooleans[i]);
389                                break;
390                        case 3:
391                                sdr.setWednesday(dayBooleans[i]);
392                                break;
393                        case 4:
394                                sdr.setThursday(dayBooleans[i]);
395                                break;
396                        case 5:
397                                sdr.setFriday(dayBooleans[i]);
398                                break;
399                        case 6:
400                                sdr.setSaturday(dayBooleans[i]);
401                                break;
402                        }
403                }
404
405                service.saveOrUpdate(sdr);
406
407                ShiftDifferentialRule sdrBack = service.getShiftDifferentialRule(sdr.getTkShiftDiffRuleId());
408
409        DateTimeZone tz = HrServiceLocator.getTimezoneService().getUserTimezoneWithFallback();
410        LocalTime orig_start = new LocalTime(sdr.getBeginTime());
411                LocalTime orig_end = new LocalTime(sdr.getEndTime());
412
413                LocalTime stored_start = new LocalTime(sdrBack.getBeginTime());
414                LocalTime stored_end = new LocalTime(sdrBack.getEndTime());
415
416                Assert.assertTrue("Start times not equal.", orig_start.equals(stored_start));
417                Assert.assertTrue("End times not equal.", orig_end.equals(stored_end));
418        }
419
420
421    @Ignore
422    @Test
423    /**
424     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
425     *
426     * Create a timeblock on two days, one day has normal REG shift eligible
427     * hours, one day has HOL time.
428     *
429     * Modified version of the simple case, SDR from 12:00 to 17:00, every day,
430     * must have at least 4 hours with a maximum 15 minute gap.
431     *
432     */
433    public void simpleCaseWithWorkSchedule() throws Exception {
434                // Create the Rule
435                boolean[] dayArray = {true, true, true, true, true, true, true};
436                // Matches HR Job ID #1 (job # 30)
437                Long jobNumber = 30L;
438                Long workArea = 0L;
439                this.createShiftDifferentialRule(
440                                "BWS-CAL",
441                                "REG",
442                                "PRM",
443                                "IN",  // changed from "SD1" to "IN" for changes of adding groupKeyCode to Job
444                                "SD1",
445                                "SD1",
446                                (new LocalTime(12, 0)),
447                                (new LocalTime(17, 0)),
448                                new BigDecimal(4), // minHours
449                                new BigDecimal("15.00"), // maxGap
450                                dayArray);
451
452                // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
453                DateTime start = new DateTime(2010, 3, 29, 12, 0, 0, 0, TKUtils.getSystemDateTimeZone());
454        DateTime holtime = new DateTime(2010, 3, 30, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
455                List<TimeBlock> blocks = new ArrayList<TimeBlock>();
456                CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
457                blocks.addAll(TkTestUtils.createUniformTimeBlocks(start,   1, new BigDecimal("4"), "REG", jobNumber, workArea));
458        blocks.addAll(TkTestUtils.createUniformTimeBlocks(holtime, 1, new BigDecimal("4"), "HOL", jobNumber, workArea));
459
460                TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
461
462                // Verify pre-Rule Run
463                TkTestUtils.verifyAggregateHourSums("Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("REG", new BigDecimal(4));put("HOL", new BigDecimal(4));}},aggregate,2);
464
465                // Run Rule
466                TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
467                tdoc.setTimeBlocks(blocks);
468                TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
469
470                // Verify post-Rule Run
471                TkTestUtils.verifyAggregateHourSums("Post Rules Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal(8));put("REG", new BigDecimal(4));}},aggregate,2);
472
473    }
474
475
476
477    /**
478     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
479     *
480     * Create a single 24 hour timeblock that spans two different shift, both exceeding the min hours
481     */
482    @Test
483    public void overlapMultipleShiftsWithSameTimeBlock() {
484        // Create the Rule
485        boolean[] dayArray = {true, true, true, true, true, true, true};
486        // Matches HR Job ID #1 (job # 30)
487        Long jobNumber = 30L;
488        Long workArea = 0L;
489        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
490
491        //3pm to 8am, 6 hour minimum, 90 minute max gap
492        this.createShiftDifferentialRule(
493                "BWS-CAL",
494                "REG",
495                "PRM",
496                "IN",
497                "SD1",
498                "SD1",
499                (new LocalTime(15, 0)),
500                (new LocalTime(8, 0)),
501                new BigDecimal(6), // minHours
502                new BigDecimal("90.00"), // maxGap
503                dayArray);
504
505        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
506        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
507        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
508
509        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
510        DateTime tbStart = new DateTime(2010, 3, 29, 0, 0, 0, 0, zone);
511
512        //24 time block (midnight to midnight)
513        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart, 1, new BigDecimal("24"), "REG", jobNumber, workArea));
514
515        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
516
517        // Verify pre-Rule Run
518        TkTestUtils.verifyAggregateHourSums("Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("REG", new BigDecimal(24));}},aggregate,2);
519
520        // Run Rule
521        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
522        tdoc.setTimeBlocks(blocks);
523        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
524
525        // Verify post-Rule Run
526        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal(17));put("REG", new BigDecimal(24));}},aggregate,2);
527
528    }
529
530    /**
531     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
532     *
533     * Create a two 24 hour timeblocks that span three different shifts, all exceeding the min hours
534     */
535    @Test
536    public void overlapMultipleShiftsWithMultipleTimeBlocks() {
537        // Create the Rule
538        boolean[] dayArray = {true, true, true, true, true, true, true};
539        // Matches HR Job ID #1 (job # 30)
540        Long jobNumber = 30L;
541        Long workArea = 0L;
542        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
543
544        //3pm to 8am, 6 hour minimum, 90 minute max gap
545        this.createShiftDifferentialRule(
546                "BWS-CAL",
547                "REG",
548                "PRM",
549                "IN",
550                "SD1",
551                "SD1",
552                (new LocalTime(15, 0)),
553                (new LocalTime(8, 0)),
554                new BigDecimal(6), // minHours
555                new BigDecimal("90.00"), // maxGap
556                dayArray);
557
558        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
559        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
560        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
561
562        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
563        DateTime tbStart = new DateTime(2010, 3, 29, 0, 0, 0, 0, zone);
564
565        //24 time block (midnight to midnight)
566        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart, 1, new BigDecimal("24"), "REG", jobNumber, workArea));
567
568        //24 time block (midnight to midnight)
569        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart.plusDays(1), 1, new BigDecimal("24"), "REG", jobNumber, workArea));
570
571        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
572
573        // Verify pre-Rule Run
574        TkTestUtils.verifyAggregateHourSums("Pre-Check", new HashMap<String,BigDecimal>() {{put("PRM", BigDecimal.ZERO);put("REG", new BigDecimal(48));}},aggregate,2);
575
576        // Run Rule
577        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
578        tdoc.setTimeBlocks(blocks);
579        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
580
581        // Verify post-Rule Run
582        //overlaps from 12a-8a (8 hours), 3p-8a (17 hours), and 3p - 12a (9 hours) == 34 hours
583        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new HashMap<String,BigDecimal>() {{put("PRM", new BigDecimal(34));put("REG", new BigDecimal(48));}},aggregate,2);
584
585    }
586
587    /**
588     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
589     *
590     * Create a single 24 hour timeblock that spans two different shift, both exceeding the min hours
591     */
592    @Test
593    public void overlapMultipleShiftsWithSameTimeBlockExceedingMinOnOneShift() {
594        // Create the Rule
595        boolean[] dayArray = {true, true, true, true, true, true, true};
596        // Matches HR Job ID #1 (job # 30)
597        Long jobNumber = 30L;
598        Long workArea = 0L;
599        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
600
601        //3pm to 8am, 6 hour minimum, 90 minute max gap
602        this.createShiftDifferentialRule(
603                "BWS-CAL",
604                "REG",
605                "PRM",
606                "IN",
607                "SD1",
608                "SD1",
609                (new LocalTime(15, 0)),
610                (new LocalTime(8, 0)),
611                new BigDecimal(6), // minHours
612                new BigDecimal("90.00"), // maxGap
613                dayArray);
614
615        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
616        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
617        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
618
619        //reg timeblock 3am - midnight
620        DateTime tbStart = new DateTime(2010, 3, 30, 3, 0, 0, 0, zone);
621        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart,   1, new BigDecimal("21"), "REG", jobNumber, workArea));
622
623        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
624
625        // Verify pre-Rule Run
626        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
627                .put("PRM", BigDecimal.ZERO)
628                .put("REG", new BigDecimal(21)).build(), aggregate, 2);
629
630        // Run Rule
631        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
632        tdoc.setTimeBlocks(blocks);
633        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
634
635        // Verify post-Rule Run
636        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
637                .put("PRM", new BigDecimal(9))
638                .put("REG", new BigDecimal(21)).build(),
639                aggregate,
640                2);
641
642    }
643
644    /**
645     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
646     *
647     * Create a single 24 hour timeblock that spans two different shift, both exceeding the min hours
648     */
649    @Test
650    public void overlapMultipleShiftsWithSameTimeBlockExceedingMinOnFirstShift() {
651        // Create the Rule
652        boolean[] dayArray = {true, true, true, true, true, true, true};
653        // Matches HR Job ID #1 (job # 30)
654        Long jobNumber = 30L;
655        Long workArea = 0L;
656        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
657
658        //3pm to 8am, 6 hour minimum, 90 minute max gap
659        this.createShiftDifferentialRule(
660                "BWS-CAL",
661                "REG",
662                "PRM",
663                "IN",
664                "SD1",
665                "SD1",
666                (new LocalTime(15, 0)),
667                (new LocalTime(8, 0)),
668                new BigDecimal(6), // minHours
669                new BigDecimal("90.00"), // maxGap
670                dayArray);
671
672        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
673        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
674        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
675
676        //reg timeblock 1am - 9pm
677        DateTime tbStart = new DateTime(2010, 3, 30, 1, 0, 0, 0, zone);
678        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart,   1, new BigDecimal("19"), "REG", jobNumber, workArea));
679
680        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
681
682        // Verify pre-Rule Run
683        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
684                .put("PRM", BigDecimal.ZERO)
685                .put("REG", new BigDecimal(19)).build(), aggregate, 2);
686
687        // Run Rule
688        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
689        tdoc.setTimeBlocks(blocks);
690        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
691
692        // Verify post-Rule Run
693        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
694                        .put("PRM", new BigDecimal(7))
695                        .put("REG", new BigDecimal(19)).build(),
696                aggregate,
697                2);
698
699    }
700
701    /**
702     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
703     *
704     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
705     */
706    @Test
707    public void overlapMultipleShiftsWithSameTimeBlocNeitherExceedingMin() {
708        // Create the Rule
709        boolean[] dayArray = {true, true, true, true, true, true, true};
710        // Matches HR Job ID #1 (job # 30)
711        Long jobNumber = 30L;
712        Long workArea = 0L;
713        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
714
715        //3pm to 8am, 6 hour minimum, 90 minute max gap
716        this.createShiftDifferentialRule(
717                "BWS-CAL",
718                "REG",
719                "PRM",
720                "IN",
721                "SD1",
722                "SD1",
723                (new LocalTime(15, 0)),
724                (new LocalTime(8, 0)),
725                new BigDecimal(6), // minHours
726                new BigDecimal("90.00"), // maxGap
727                dayArray);
728
729        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
730        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
731        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
732        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
733
734        //reg timeblock 3am - 8pm
735        DateTime tbStart = new DateTime(2010, 3, 30, 3, 0, 0, 0, zone);
736        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart,   1, new BigDecimal("17"), "REG", jobNumber, workArea));
737
738        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
739
740        // Verify pre-Rule Run
741        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
742                .put("PRM", BigDecimal.ZERO)
743                .put("REG", new BigDecimal(17)).build(), aggregate, 2);
744
745        // Run Rule
746        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
747        tdoc.setTimeBlocks(blocks);
748        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
749
750        // Verify post-Rule Run
751        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
752                        .put("PRM", BigDecimal.ZERO)
753                        .put("REG", new BigDecimal(17)).build(),
754                aggregate,
755                2);
756    }
757
758    /**
759     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
760     *
761     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
762     */
763    @Test
764    public void multipleTimeBlocksOvernightExceedingMin() {
765        // Create the Rule
766        boolean[] dayArray = {true, true, true, true, true, true, true};
767        // Matches HR Job ID #1 (job # 30)
768        Long jobNumber = 30L;
769        Long workArea = 0L;
770        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
771
772        //3pm to 8am, 6 hour minimum, 90 minute max gap
773        this.createShiftDifferentialRule(
774                "BWS-CAL",
775                "REG",
776                "PRM",
777                "IN",
778                "SD1",
779                "SD1",
780                (new LocalTime(15, 0)),
781                (new LocalTime(8, 0)),
782                new BigDecimal(6), // minHours
783                new BigDecimal("90.00"), // maxGap
784                dayArray);
785
786        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
787        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
788        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
789        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
790
791        //reg timeblock 10pm - midnight
792        DateTime tbStart1 = new DateTime(2010, 3, 30, 22, 0, 0, 0, zone);
793        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart1,   1, new BigDecimal("2"), "REG", jobNumber, workArea));
794        //reg timeblock midnight - 5am
795        DateTime tbStart2 = new DateTime(2010, 3, 31, 0, 0, 0, 0, zone);
796        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart2,   1, new BigDecimal("5"), "REG", jobNumber, workArea));
797
798        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
799
800        // Verify pre-Rule Run
801        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
802                .put("PRM", BigDecimal.ZERO)
803                .put("REG", BigDecimal.valueOf(7)).build(), aggregate, 2);
804
805        // Run Rule
806        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
807        tdoc.setTimeBlocks(blocks);
808        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
809
810        // Verify post-Rule Run
811        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
812                        .put("PRM", BigDecimal.valueOf(7))
813                        .put("REG", BigDecimal.valueOf(7)).build(),
814                aggregate,
815                2);
816    }
817
818    /**
819     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
820     *
821     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
822     */
823    @Test
824    public void multipleTimeBlocksOvernightExceedingMinWithSixtyMinuteGap() {
825        // Create the Rule
826        boolean[] dayArray = {true, true, true, true, true, true, true};
827        // Matches HR Job ID #1 (job # 30)
828        Long jobNumber = 30L;
829        Long workArea = 0L;
830        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
831
832        //3pm to 8am, 6 hour minimum, 90 minute max gap
833        this.createShiftDifferentialRule(
834                "BWS-CAL",
835                "REG",
836                "PRM",
837                "IN",
838                "SD1",
839                "SD1",
840                (new LocalTime(15, 0)),
841                (new LocalTime(8, 0)),
842                new BigDecimal(6), // minHours
843                new BigDecimal("90.00"), // maxGap
844                dayArray);
845
846        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
847        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
848        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
849        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
850
851        //reg timeblock 10pm - midnight
852        DateTime tbStart1 = new DateTime(2010, 3, 30, 22, 0, 0, 0, zone);
853        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart1,   1, new BigDecimal("2"), "REG", jobNumber, workArea));
854        //reg timeblock 1am - 5am
855        DateTime tbStart2 = new DateTime(2010, 3, 31, 1, 0, 0, 0, zone);
856        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart2,   1, new BigDecimal("4"), "REG", jobNumber, workArea));
857
858        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
859
860        // Verify pre-Rule Run
861        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
862                .put("PRM", BigDecimal.ZERO)
863                .put("REG", BigDecimal.valueOf(6)).build(), aggregate, 2);
864
865        // Run Rule
866        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
867        tdoc.setTimeBlocks(blocks);
868        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
869
870        // Verify post-Rule Run
871        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
872                        .put("PRM", BigDecimal.valueOf(6))
873                        .put("REG", BigDecimal.valueOf(6)).build(),
874                aggregate,
875                2);
876    }
877
878    /**
879     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
880     *
881     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
882     */
883    @Test
884    public void multipleTimeBlocksOvernightExceedingMinWithNinetyMinuteGap() {
885        // Create the Rule
886        boolean[] dayArray = {true, true, true, true, true, true, true};
887        // Matches HR Job ID #1 (job # 30)
888        Long jobNumber = 30L;
889        Long workArea = 0L;
890        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
891
892        //3pm to 8am, 6 hour minimum, 90 minute max gap
893        this.createShiftDifferentialRule(
894                "BWS-CAL",
895                "REG",
896                "PRM",
897                "IN",
898                "SD1",
899                "SD1",
900                (new LocalTime(15, 0)),
901                (new LocalTime(8, 0)),
902                new BigDecimal(6), // minHours
903                new BigDecimal("90.00"), // maxGap
904                dayArray);
905
906        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
907        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
908        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
909        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
910
911        //reg timeblock 10pm - midnight
912        DateTime tbStart1 = new DateTime(2010, 3, 30, 22, 0, 0, 0, zone);
913        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart1,   1, new BigDecimal("2"), "REG", jobNumber, workArea));
914        //reg timeblock 1:30am - 5:30pm
915        DateTime tbStart2 = new DateTime(2010, 3, 31, 1, 30, 0, 0, zone);
916        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart2,   1, new BigDecimal("4"), "REG", jobNumber, workArea));
917
918        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
919
920        // Verify pre-Rule Run
921        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
922                .put("PRM", BigDecimal.ZERO)
923                .put("REG", BigDecimal.valueOf(6)).build(), aggregate, 2);
924
925        // Run Rule
926        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
927        tdoc.setTimeBlocks(blocks);
928        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
929
930        // Verify post-Rule Run
931        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
932                        .put("PRM", BigDecimal.valueOf(6))
933                        .put("REG", BigDecimal.valueOf(6)).build(),
934                aggregate,
935                2);
936    }
937
938    /**
939     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
940     *
941     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
942     */
943    @Test
944    public void multipleTimeBlocksOvernightExceedingMinButExceedingGap() {
945        // Create the Rule
946        boolean[] dayArray = {true, true, true, true, true, true, true};
947        // Matches HR Job ID #1 (job # 30)
948        Long jobNumber = 30L;
949        Long workArea = 0L;
950        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
951
952        //3pm to 8am, 6 hour minimum, 90 minute max gap
953        this.createShiftDifferentialRule(
954                "BWS-CAL",
955                "REG",
956                "PRM",
957                "IN",
958                "SD1",
959                "SD1",
960                (new LocalTime(15, 0)),
961                (new LocalTime(8, 0)),
962                new BigDecimal(6), // minHours
963                new BigDecimal("90.00"), // maxGap
964                dayArray);
965
966        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
967        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
968        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
969        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
970
971        //reg timeblock 10pm - midnight
972        DateTime tbStart1 = new DateTime(2010, 3, 30, 22, 0, 0, 0, zone);
973        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart1,   1, new BigDecimal("2"), "REG", jobNumber, workArea));
974        //reg timeblock 1:36am - 5:36am
975        DateTime tbStart2 = new DateTime(2010, 3, 31, 1, 36, 0, 0, zone);
976        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart2,   1, new BigDecimal("4"), "REG", jobNumber, workArea));
977
978        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
979
980        // Verify pre-Rule Run
981        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
982                .put("PRM", BigDecimal.ZERO)
983                .put("REG", BigDecimal.valueOf(6)).build(), aggregate, 2);
984
985        // Run Rule
986        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
987        tdoc.setTimeBlocks(blocks);
988        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
989
990        // Verify post-Rule Run
991        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
992                        .put("PRM", BigDecimal.valueOf(0))
993                        .put("REG", BigDecimal.valueOf(6)).build(),
994                aggregate,
995                2);
996    }
997
998
999    /**
1000     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
1001     *
1002     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
1003     */
1004    @Test
1005    public void threeBlocksWithinSameShift() {
1006        // Create the Rule
1007        boolean[] dayArray = {true, true, true, true, true, true, true};
1008        // Matches HR Job ID #1 (job # 30)
1009        Long jobNumber = 30L;
1010        Long workArea = 0L;
1011        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
1012
1013        //3pm to 8am, 6 hour minimum, 90 minute max gap
1014        this.createShiftDifferentialRule(
1015                "BWS-CAL",
1016                "REG",
1017                "PRM",
1018                "IN",
1019                "SD1",
1020                "SD1",
1021                (new LocalTime(15, 0)),
1022                (new LocalTime(8, 0)),
1023                new BigDecimal(6), // minHours
1024                new BigDecimal("90.00"), // maxGap
1025                dayArray);
1026
1027        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
1028        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
1029        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
1030
1031        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
1032
1033        //reg timeblock 3pm - 5pm
1034        DateTime tbStart1 = new DateTime(2010, 3, 30, 15, 0, 0, 0, zone);
1035        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart1,   1, new BigDecimal("2"), "REG", jobNumber, workArea));
1036
1037        // 6pm - 9pm
1038        DateTime tbStart2 = new DateTime(2010, 3, 30, 18, 0, 0, 0, zone);
1039        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart2,   1, new BigDecimal("3"), "REG", jobNumber, workArea));
1040
1041        // 10pm - 11pm
1042        DateTime tbStart3 = new DateTime(2010, 3, 30, 22, 0, 0, 0, zone);
1043        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart3,   1, new BigDecimal("1"), "REG", jobNumber, workArea));
1044
1045        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
1046
1047        // Verify pre-Rule Run
1048        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
1049                .put("PRM", BigDecimal.ZERO)
1050                .put("REG", BigDecimal.valueOf(6)).build(), aggregate, 2);
1051
1052        // Run Rule
1053        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
1054        tdoc.setTimeBlocks(blocks);
1055        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
1056
1057        // Verify post-Rule Run
1058        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
1059                        .put("PRM", BigDecimal.valueOf(6))
1060                        .put("REG", BigDecimal.valueOf(6)).build(),
1061                aggregate,
1062                2);
1063    }
1064
1065    /**
1066     * Tests WorkSchedules impact on Shift Differential Rule: Simple Case
1067     *
1068     * Create a single 17 hour timeblock that spans two different shift, neither exceeding the min hours
1069     */
1070    @Test
1071    public void fourBlocksWithinSameShiftSpanningTwoDays() {
1072        // Create the Rule
1073        boolean[] dayArray = {true, true, true, true, true, true, true};
1074        // Matches HR Job ID #1 (job # 30)
1075        Long jobNumber = 30L;
1076        Long workArea = 0L;
1077        DateTimeZone zone = HrServiceLocator.getTimezoneService().getTargetUserTimezoneWithFallback();
1078
1079        //3pm to 8am, 6 hour minimum, 90 minute max gap
1080        this.createShiftDifferentialRule(
1081                "BWS-CAL",
1082                "REG",
1083                "PRM",
1084                "IN",
1085                "SD1",
1086                "SD1",
1087                (new LocalTime(15, 0)),
1088                (new LocalTime(8, 0)),
1089                new BigDecimal(6), // minHours
1090                new BigDecimal("90.00"), // maxGap
1091                dayArray);
1092
1093        // Create Time Blocks (2 days, 2 blocks on each day, 15 minute gap between blocks, 4 hours total each.
1094        DateTime start = new DateTime(2010, 3, 29, 0, 0, 0, 0, TKUtils.getSystemDateTimeZone());
1095        CalendarEntry payCalendarEntry =  HrServiceLocator.getCalendarEntryService().getCurrentCalendarDates("admin", start);
1096
1097        List<TimeBlock> blocks = new ArrayList<TimeBlock>();
1098
1099        //reg timeblock 5pm - 6pm
1100        DateTime tbStart1 = new DateTime(2010, 3, 30, 17, 0, 0, 0, zone);
1101        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart1,   1, new BigDecimal("1"), "REG", jobNumber, workArea));
1102
1103        // 7:30pm - 8:30pm
1104        DateTime tbStart2 = new DateTime(2010, 3, 30, 19, 30, 0, 0, zone);
1105        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart2,   1, new BigDecimal("1"), "REG", jobNumber, workArea));
1106
1107        // 10pm - midnight
1108        DateTime tbStart3 = new DateTime(2010, 3, 30, 22, 0, 0, 0, zone);
1109        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart3,   1, new BigDecimal("2"), "REG", jobNumber, workArea));
1110
1111        // 1:30am - 4:30am
1112        DateTime tbStart4 = new DateTime(2010, 3, 31, 1, 30, 0, 0, zone);
1113        blocks.addAll(TkTestUtils.createUniformTimeBlocks(tbStart4,   1, new BigDecimal("3"), "REG", jobNumber, workArea));
1114
1115        TkTimeBlockAggregate aggregate = new TkTimeBlockAggregate(blocks, payCalendarEntry);
1116
1117        // Verify pre-Rule Run
1118        TkTestUtils.verifyAggregateHourSums("Pre-Check", new ImmutableMap.Builder<String, BigDecimal>()
1119                .put("PRM", BigDecimal.ZERO)
1120                .put("REG", BigDecimal.valueOf(7)).build(), aggregate, 2);
1121
1122        // Run Rule
1123        TimesheetDocument tdoc = TkTestUtils.populateBlankTimesheetDocument(start, "admin");
1124        tdoc.setTimeBlocks(blocks);
1125        TkServiceLocator.getShiftDifferentialRuleService().processShiftDifferentialRules(tdoc, aggregate);
1126
1127        // Verify post-Rule Run
1128        TkTestUtils.verifyAggregateHourSums("Post Rules Check", new ImmutableMap.Builder<String, BigDecimal>()
1129                        .put("PRM", BigDecimal.valueOf(7))
1130                        .put("REG", BigDecimal.valueOf(7)).build(),
1131                aggregate,
1132                2);
1133    }
1134}