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.flsa;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.joda.time.DateTime;
020    import org.joda.time.DateTimeZone;
021    import org.joda.time.Interval;
022    import org.joda.time.LocalDateTime;
023    import org.kuali.hr.time.timeblock.TimeBlock;
024    import org.kuali.hr.time.timeblock.TimeHourDetail;
025    import org.kuali.hr.time.util.TKUtils;
026    import org.kuali.hr.time.util.TkConstants;
027    
028    import java.math.BigDecimal;
029    import java.util.ArrayList;
030    import java.util.HashMap;
031    import java.util.List;
032    import java.util.Map;
033    
034    public class FlsaDay {
035            private Map<String,BigDecimal> earnCodeToHours = new HashMap<String,BigDecimal>();
036            private Map<String,List<TimeBlock>> earnCodeToTimeBlocks = new HashMap<String,List<TimeBlock>>();
037            private List<TimeBlock> appliedTimeBlocks = new ArrayList<TimeBlock>();
038    
039            Interval flsaDateInterval;
040            LocalDateTime flsaDate;
041        DateTimeZone timeZone;
042    
043        /**
044         *
045         * @param flsaDate A LocalDateTime because we want to be conscious of the
046         * relative nature of this flsa/window
047         * @param timeBlocks
048         * @param timeZone The timezone we are constructing, relative.
049         */
050            public FlsaDay(LocalDateTime flsaDate, List<TimeBlock> timeBlocks, DateTimeZone timeZone) {
051                    this.flsaDate = flsaDate;
052            this.timeZone = timeZone;
053                    flsaDateInterval = new Interval(flsaDate.toDateTime(timeZone), flsaDate.toDateTime(timeZone).plusHours(24));
054                    this.setTimeBlocks(timeBlocks);
055            }
056    
057            /**
058             * Handles the breaking apart of existing time blocks around FLSA boundaries.
059             *
060             * This method will compare the FLSA interval against the timeblock interval
061             * to determine how many hours overlap.  It will then examine the time hour
062             * details
063             *
064             * @param timeBlocks a sorted list of time blocks.
065             */
066            public void setTimeBlocks(List<TimeBlock> timeBlocks) {
067                    for (TimeBlock block : timeBlocks)
068                            if (!applyBlock(block, this.appliedTimeBlocks))
069                                    break;
070            }
071    
072            /**
073             * This method will compute the mappings present for this object:
074             *
075             * earnCodeToTimeBlocks
076             * earnCodeToHours
077             *
078             */
079            public void remapTimeHourDetails() {
080                    List<TimeBlock> reApplied = new ArrayList<TimeBlock>(appliedTimeBlocks.size());
081                    earnCodeToHours.clear();
082                    earnCodeToTimeBlocks.clear();
083                    for (TimeBlock block : appliedTimeBlocks) {
084                            applyBlock(block, reApplied);
085                    }
086            }
087    
088            /**
089         * This method determines if the provided TimeBlock is applicable to this
090         * FLSA day, and if so will add it to the applyList. It could be the case
091         * that a TimeBlock is on the boundary of the FLSA day so that only a
092         * partial amount of the hours for that TimeBlock will count towards this
093         * day.
094         *
095         * |---------+------------------+---------|
096         * | Day 1   | Day 1/2 Boundary | Day 2   |
097         * |---------+------------------+---------|
098         * | Block 1 |             | Block 2      |
099         * |---------+------------------+---------|
100         *
101         * The not so obvious ascii diagram above is intended to illustrate the case
102         * where on day one you have 1 fully overlapping time block (block1) and one
103         * partially overlapping time block (block2). Block 2 belongs to both FLSA
104         * Day 1 and Day 2.
105         *
106             * @param block A time block that we want to check and apply to this day.
107             * @param applyList A list of time blocks we want to add applicable time blocks to.
108             *
109             * @return True if the block is applicable, false otherwise.  The return
110             * value can be used as a quick exit for the setTimeBlocks() method.
111             *
112             * TODO : Bucketing of partial FLSA days is still suspect, however real life examples of this are likely non-existent to rare.
113         *
114         * Danger may still lurk in day-boundary overlapping time blocks that have multiple Time Hour Detail entries.
115             */
116            private boolean applyBlock(TimeBlock block, List<TimeBlock> applyList) {
117                    DateTime beginDateTime = new DateTime(block.getBeginTimestamp(), this.timeZone);
118                    DateTime endDateTime = new DateTime(block.getEndTimestamp(), this.timeZone);
119    
120                    if (beginDateTime.isAfter(flsaDateInterval.getEnd()))
121                            return false;
122    
123                    Interval timeBlockInterval = null;
124                    //Requested to have zero hour time blocks be able to be added to the GUI
125                    boolean zeroHoursTimeBlock = false;
126                    if(endDateTime.getMillis() > beginDateTime.getMillis()){
127                            timeBlockInterval = new Interval(beginDateTime,endDateTime);
128                    }
129                    
130                    if(flsaDateInterval.contains(beginDateTime)){
131                            zeroHoursTimeBlock = true;
132                    }
133    
134                    Interval overlapInterval = flsaDateInterval.overlap(timeBlockInterval);
135                    long overlap = (overlapInterval == null) ? 0L : overlapInterval.toDurationMillis();
136                    BigDecimal overlapHours = TKUtils.convertMillisToHours(overlap);
137                    if((overlapHours.compareTo(BigDecimal.ZERO) == 0) && flsaDateInterval.contains(beginDateTime) && flsaDateInterval.contains(endDateTime)){
138                            if(block.getHours().compareTo(BigDecimal.ZERO) > 0){
139                                    overlapHours = block.getHours();
140                            }
141                    }
142    
143            // Local lookup for this time-block to ensure we are not over applicable hours.
144            // You will notice below we are earn codes globally per day, and also locally per timeblock.
145            // The local per-time block mapping is used only to verify that we have not gone over allocated overlap time
146            // for the individual time block.
147            Map<String,BigDecimal> localEarnCodeToHours = new HashMap<String,BigDecimal>();
148    
149                    if (zeroHoursTimeBlock || overlapHours.compareTo(BigDecimal.ZERO) > 0 || (flsaDateInterval.contains(beginDateTime) && StringUtils.equals(block.getEarnCodeType(),TkConstants.EARN_CODE_AMOUNT)))  {
150    
151                List<TimeHourDetail> details = block.getTimeHourDetails();
152                for (TimeHourDetail thd : details) {
153                    BigDecimal ecHours = earnCodeToHours.containsKey(thd.getEarnCode()) ? earnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO;
154                    BigDecimal localEcHours = localEarnCodeToHours.containsKey(thd.getEarnCode()) ? localEarnCodeToHours.get(thd.getEarnCode()) : BigDecimal.ZERO;
155                    //NOTE adding this in the last few hours before release.. remove if side effects are noticed
156                    if (overlapHours.compareTo(localEcHours) >= 0 || thd.getAmount().compareTo(BigDecimal.ZERO) == 0) {
157                        ecHours = ecHours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
158                        localEcHours = localEcHours.add(thd.getHours(), TkConstants.MATH_CONTEXT);
159                        earnCodeToHours.put(thd.getEarnCode(), ecHours);
160                        localEarnCodeToHours.put(thd.getEarnCode(), localEcHours);
161                    }
162                }
163    
164                            List<TimeBlock> blocks = earnCodeToTimeBlocks.get(block.getEarnCode());
165                            if (blocks == null) {
166                                    blocks = new ArrayList<TimeBlock>();
167                                    earnCodeToTimeBlocks.put(block.getEarnCode(), blocks);
168                            }
169                            blocks.add(block);
170                            applyList.add(block);
171                    }
172    
173                    return true;
174            }
175    
176            public Map<String, BigDecimal> getEarnCodeToHours() {
177                    return earnCodeToHours;
178            }
179    
180            public Map<String, List<TimeBlock>> getEarnCodeToTimeBlocks() {
181                    return earnCodeToTimeBlocks;
182            }
183    
184            public void setEarnCodeToTimeBlocks(Map<String, List<TimeBlock>> earnCodeToTimeBlocks) {
185                    this.earnCodeToTimeBlocks = earnCodeToTimeBlocks;
186            }
187    
188            public List<TimeBlock> getAppliedTimeBlocks() {
189                    return appliedTimeBlocks;
190            }
191    
192            public void setAppliedTimeBlocks(List<TimeBlock> appliedTimeBlocks) {
193                    this.appliedTimeBlocks = appliedTimeBlocks;
194            }
195    
196    
197    }